Flutter笔记:图片的 precacheImage 函数

news2024/12/23 19:49:10
Flutter笔记
图片的 precacheImage 函数

作者李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/134004572


【简介】:precacheImage 是 Flutter 中提供的一个函数。顾名思义,pre预先、cache缓存、Image图片,很容易看出 precacheImage 函数用于预加载图像并将其缓存到图像缓存中。这个函数是非常有用的,因为它可以在用户实际需要显示图像之前,提前将图像加载到内存中,以提高图像的加载速度。这对于提供更好的用户体验和性能非常重要,特别是当项目中有多个图像需要显示时。


1. 从需求说起

当我使用 PageView 实现轮播时,用到了 Image.network 构造函数下载图片。但是页面初始化后,仅仅加载了一张图,轮播还未显示到的图片有一点“懒”加载的意思,这大大降低了用户体验。如图所示:
在这里插入图片描述
可以看到,每次滚动到另外一张图片时,出现了明显的“白屏”。为了解决这个问题,我们先明确期待的效果是什么样的:
我们期待在进入页面时预先缓存所有页面,从而避免白屏等待。效果如下:

在这里插入图片描述

2. precacheImage 函数

precacheImage 是 Flutter 中提供的一个函数。顾名思义,pre预先、cache缓存、Image图片,很容易看出 precacheImage 函数用于预加载图像并将其缓存到图像缓存中。这个函数是非常有用的,因为它可以在用户实际需要显示图像之前,提前将图像加载到内存中,以提高图像的加载速度。这对于提供更好的用户体验和性能非常重要,特别是当项目中有多个图像需要显示时。

接下来,我们看一下关于 precacheImage 函数的一些重要信息和用法。

2.1 参数

  • provider: 要预加载的图像的提供程序,通常是 AssetImageNetworkImageFileImage
  • context: 当前的 BuildContext,用于获取应用程序的配置信息,如屏幕分辨率和设备像素比例等。
  • size(可选):一个可选的 Size 对象,用于指定要加载的图像的大小。
  • onError(可选):一个可选的回调函数,用于处理在预加载期间出现的错误。

2.2 返回值

  • Future<void>precacheImage 返回一个 Future,该 Future 将在第一个图像加载成功或失败时完成。它不会抛出错误,即使预加载失败,也需要使用 onError 参数手动处理错误。

2.3 缓存和预加载

  • 预加载的图像将被存储在 Flutter 的图像缓存中,这意味着它们将在内存中可供快速访问,以便稍后使用。
  • 缓存可以提高图像的加载速度,因为图像在内存中等待着,当需要显示时,它们会立即可用。
  • 不需要使用相同的 ImageProvider 实例来访问预加载的图像,只要它们具有相同的键(通常是图像的URL或文件路径),缓存就可以找到它们。

2.4 生命周期和内存管理

  • ImageCache 会保存传递给 precacheImage 的所有图像,只要它们的 ImageStreamCompleter 至少有一个监听器。
  • 如果图像未完成加载,precacheImage 将等到未来完成,并在其未来完成后的帧末尾,释放其自己的监听器。这会给调用者一个机会来订阅流,如果需要的话。
  • 如果调用者希望保留解析后的图像在内存中,可以调用 provider.resolve 并添加一个监听器到返回的 ImageStream。图像将在调用者从流中移除其监听器之前,至少保留在内存中,即使它本来不适合缓存。

2.5 潜在问题

  • 需要小心在内存中固定大型图像或大量图像,因为这可能导致内存不足,最终被操作系统终止。
  • 内存不足问题可能会导致应用程序立即崩溃,有时甚至没有其他错误消息。

通过使用 precacheImage 函数,您可以更好地控制图像加载的时机,从而提高应用程序的性能和用户体验。这对于需要加载大量图像或希望在用户看到图像之前就将其准备好的应用程序特别有用。

3. precacheImage 源码

/// 预加载图像到图像缓存中
///
/// 返回一个 [Future],当 [ImageProvider] 提供的第一个图像可用或加载失败时将完成
///
/// 如果稍后会被 [Image] 或 [BoxDecoration] 或 [FadeInImage] 使用,它可能会加载得更快
/// 图像的使用者不需要使用相同的 [ImageProvider] 实例。只要这两个图像共享相同的键,并且图像由缓存保存,
/// [ImageCache] 就可以找到图像
///
/// 如果缓存被禁用,图像太大或由自定义 [ImageCache] 实现的其他标准实施,则缓存可能会拒绝保存图像
///
/// [ImageCache] 将保存传递给 [ImageCache.putIfAbsent] 的所有图像,只要它们的 [ImageStreamCompleter] 至少有一个监听器
/// 这个方法将等到它的未来完成后的帧末尾,然后释放它自己的监听器。这为调用者提供了监听流的机会,如果需要的话
/// 调用者可以通过调用 [ImageProvider.obtainCacheStatus] 来确定图像是否最终保存在缓存中
/// 如果它只被保存为 [ImageCacheStatus.live],并且调用者希望将解析后的图像保存在内存中,调用者应该立即调用
/// `provider.resolve` 并向返回的 [ImageStream] 添加一个监听器。即使它不适合缓存,该图像将至少保留在内存中,
/// 直到调用者从流中移除其监听器
///
/// 调用者应谨慎固定大图像或大量图像在内存中,因为这可能导致内存不足并被操作系统杀死
/// 可用物理内存越低,调用者越容易遇到内存不足问题。这些问题表现为进程立即死机,有时没有其他错误消息。
///
/// [BuildContext] 和 [Size] 用于选择图像配置(请参阅 [createLocalImageConfiguration])。
///
/// 返回的 future 不会以错误完成,即使预加载失败。可以使用 `onError` 参数来手动处理预加载时的错误。
///
/// 另请参见:
///
///  * [ImageCache],它保存可以重用的图像。
Future<void> precacheImage(
  ImageProvider provider,
  BuildContext context, {
  Size? size,
  ImageErrorListener? onError,
}) {
  final ImageConfiguration config = createLocalImageConfiguration(context, size: size);
  final Completer<void> completer = Completer<void>();
  final ImageStream stream = provider.resolve(config);
  ImageStreamListener? listener;
  listener = ImageStreamListener(
    (ImageInfo? image, bool sync) {
      if (!completer.isCompleted) {
        completer.complete();
      }
      // 让调用者至少到达帧的末尾来订阅图像流。
      // 请参见 ImageCache._liveImages
      SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
        stream.removeListener(listener!);
      });
    },
    onError: (Object exception, StackTrace? stackTrace) {
      if (!completer.isCompleted) {
        completer.complete();
      }
      stream.removeListener(listener!);
      if (onError != null) {
        onError(exception, stackTrace);
      } else {
        FlutterError.reportError(FlutterErrorDetails(
          context: ErrorDescription('图像无法预加载'),
          library: '图像资源服务',
          exception: exception,
          stack: stackTrace,
          silent: true,
        ));
      }
    },
  );
  stream.addListener(listener);
  return completer.future;
}

4. 注意点

正如源码注释中提醒,precacheImage 可以用于提高应用性能,但要确保仅在必要时使用它,以避免资源浪费。因此,使用该函数时主要考虑以下事项:

  • 不要滥用 precacheImage,避免预加载大量大尺寸图像,以免占用过多内存;
  • 及时释放资源:在不再需要预加载的图像时,及时释放它们,以避免不必要的内存占用。在不再需要图像时,从 ImageStream 中移除监听器,以释放内存;

另外,预加载的图像将存储在缓存中,因此缓存后需要使用相同的键来访问它们,使得你的缓存是有意义的。其中这里的相同的键,一般也就是图像的URL或文件路径。

4. 我遇到过的一个问题

为什么在生命周期函数 initState 中缓存图像有时候不选择

描述

我封装一个有状态组件 ImgCarousel类时,一开始的时候在状态生命周期方法 initState 中完成缓存,具体来说是这样的:

class _ImgCarouselState extends State<ImgCarousel> {
  // ...
  
  void initState() {
    super.initState();
    // 在initState中预加载所有图片,避免一翻一张下一张
    _precacheImages(widget.imageUrls);
  }

  void _precacheImages(List<String> imageUrls) {
    for (String imageUrl in imageUrls) {
      precacheImage(NetworkImage(imageUrl), context);
    }
  }
  // ...
}

在后续的使用中出现了下面的异常提示:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building DefaultTextStyle(debugLabel: (englishLike bodyMedium
2014).merge(((blackMountainView bodyMedium).apply).merge(unknown)), inherit: false, color:
MaterialColor(primary value: Color(0xff9e9e9e)), family: Roboto, size: 14.0, weight: 400, baseline:
alphabetic, decoration: TextDecoration.none, softWrap: wrapping at box width, overflow: clip):
dependOnInheritedWidgetOfExactType<MediaQuery>() or dependOnInheritedElement() was called before
ImgCarouselState.initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
inherited widget.
Typically references to inherited widgets should occur in widget build() methods. Alternatively,
initialization based on inherited widgets can be placed in the didChangeDependencies method, which
is called after initState and whenever the dependencies change thereafter.

异常分析

这个错误是由于在initState方法中调用了_precacheImages方法,而_precacheImages方法中使用了precacheImage来预加载图片。在预加载图片时,可能会尝试访问MediaQuery以获取设备的信息,但MediaQuery的访问应该在initState完成后进行。这导致了上述错误。

在异常提示中:

When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget’s reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
inherited widget.

大意为:当继承的组件发生变化时,例如,如果 Theme.of() 的值发生变化,那么它的依赖组件将被重新构建。如果依赖组件对继承组件的引用在构造函数或 initState() 方法中,那么重新构建的依赖组件将不会反映继承的组件。

而后面的异常提示给了一些解决办法:

Typically references to inherited widgets should occur in widget build() methods. Alternatively,
initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.

大意为

通常,对继承的组件的引用应该出现在组件 build() 方法中。或者,基于继承的组件的初始化可以放在 didChangeDependencies 方法中,该方法在initState之后以及依赖关系发生变化时调用。

回顾 didChangeDependencies 方法

didChangeDependencies是Flutter中State对象的一个生命周期方法,用于处理当依赖关系发生变化时的操作。这个方法在以下情况下被调用:

  1. State对象首次被创建时,它会立即调用didChangeDependencies方法,用于处理初始化依赖关系。
  2. 如果依赖关系发生变化,即State对象所依赖的InheritedWidget或其他继承关系发生变化,那么didChangeDependencies方法也会被调用。

通常情况下,didChangeDependencies方法用于执行一些与依赖关系相关的操作,例如:

  • 初始化或更新State对象所依赖的数据或状态。
  • 订阅全局状态的变化,以便在依赖数据变化时进行更新。
  • 执行与依赖关系相关的异步操作,例如从数据库或网络获取数据。

这个方法的常见用例包括:

  1. didChangeDependencies方法中初始化State对象所需的数据,以确保在build方法中使用这些数据时已经准备好了。
  2. 在此方法中进行全局状态的订阅,以监听状态的变化并在状态发生变化时更新State对象。
  3. 如果需要执行异步操作,如从网络获取数据,可以在didChangeDependencies中启动异步任务,并在任务完成后更新State对象,以确保数据的及时更新。

需要注意的是,didChangeDependencies方法可能会在每次build方法被调用之前被触发,因此应谨慎使用它,以避免不必要的性能开销。通常情况下,只有当依赖的数据或状态发生实际变化时才需要在didChangeDependencies中执行操作,以确保应用的正确性和性能。

解决

为了解决这个问题,您可以将_precacheImages方法的调用移到build方法中的合适位置。这样,当build方法执行时,initState已经完成,可以安全地访问MediaQuery。以下是如何修改您的代码:

class ImgCarouselState extends State<ImgCarousel> {
  // ...
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    // 在build方法中调用_precacheImages
    _precacheImages(widget.imageUrls);

    return Stack(
      children: [
        PageView.builder(
          // ... 剩余的PageView.builder配置
        ),
        Positioned(
          // ... 剩余的Positioned配置
        )
      ],
    );
  }

  void _precacheImages(List<String> imageUrls) {
    for (String imageUrl in imageUrls) {
      precacheImage(NetworkImage(imageUrl), context);
    }
  }

  // ... 
}

通过将_precacheImages方法的调用移到build方法中,您可以确保在访问MediaQuery之前initState已经完成,从而避免上述错误。

如何考虑初始化缓存时机

选择 initState?

在之前给出的代码中,_precacheImages方法用于预加载图片,以避免在滚动PageView时逐个加载图片,从而提高性能。通常情况下,首先建议在initState方法中缓存图像。毕竟initStateState对象生命周期的一部分,它在State对象创建时调用,这是进行初始化工作的好地方。预加载图片是一个初始化操作,因此将其放在initState中在很多情况下都是合适的。

但是也需要注意。将图片预加载操作放在initState中不是绝对的最佳办法。因为这可能导致一些依赖问题,特别是与MediaQuery或其他依赖有关的情况。对于一些特殊情况,将图片预加载操作延迟到 didChangeDependencies 或 build 方法中可能更合适。最佳实践会根据具体情况而有所不同。

选择 build?

如果将_precacheImages方法放在build方法中有一个缺点是,它将在每次build时都被调用。这会导致不必要的图片预加载,而且可能降低性能,因为它可能在每次build中重复执行。

选择 didChangeDependencies?

didChangeDependencies方法中也可以执行依赖关系相关的操作,但通常不建议在该方法中进行耗时的操作,因为它可能会导致UI卡顿。initState通常是更合适的地方,因为它在State对象创建后立即调用,但不会阻塞UI的渲染。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1127221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

执行 SQL 响应比较慢,你有哪些排查思路?

排查思路 如果执行 SQL 响应比较慢&#xff0c;我觉得可能有以下 4 个原因&#xff1a; 第 1 个原因&#xff1a;没有索引或者导致索引失效。 第 2 个原因&#xff1a;单表数据量数据过多&#xff0c;导致查询瓶颈第 3 个原因&#xff1a;网络原因或者机器负载过高。 第 4 个原…

JAVA-编程基础-11-02-文件流

Lison <dreamlison163.com>, v1.0.0, 2023.05.07 JAVA-编程基础-11-02-文件流 文章目录 JAVA-编程基础-11-02-文件流File 构造方法File 常用方法获取功能的方法**绝对路径和相对路径****判断功能的方法****创建、删除功能的方法**目录的遍历递归遍历 RandomAccessFile*…

LeetCode66——加一

LeetCode66——加一 题目描述&#xff1a; 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。 最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。 示例…

904. Fruit Into Baskets

904. Fruit Into Baskets 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 904. Fruit Into Baskets https://leetcode.cn/problems/fruit-into-baskets/ 完成情况&#xff1a; 解题思路&#xff1a; 连续数组 -…

Unity BatchRendererGroup 在低端设备上也实现高帧率

在这篇文章中&#xff0c;我们描述了一个小型射击游戏样本&#xff0c;它可以动画和渲染几个交互式对象。许多演示只针对高端pc&#xff0c;但这里的目标是在使用GLES 3.0的廉价手机上实现高帧率。这个例子使用了BatchRendererGroup, Burst编译器和c#作业系统。它运行在Unity 2…

Git基础教程

一、Git简介 1、什么是Git&#xff1f; Git是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或大或小的项目。 Git是Linus Torvalds为了帮助管理Linux内核开发而开发的一个开放源代码的版本控制软件。 Git与常用的版本控制工具CVS、Subversion等不同&#…

你是否能应对每天数十亿次的IP访问?Top100查找技术解析

亲爱的小伙伴们&#xff0c;大家好&#xff01;我是小米&#xff0c;一个热爱技术&#xff0c;热衷分享的IT小伙伴。今天&#xff0c;我要和大家聊一个程序员面试常见的问题&#xff1a;如何在每天海量IP访问的情况下&#xff0c;使用Java找出访问频率最高的Top 100&#xff1f…

【LeetCode刷题-数组】--344.反转字符串

344.反转字符串 使用双指针&#xff1a; class Solution {public void reverseString(char[] s) {int l 0,r s.length-1;while(l<r){char c s[l];s[l] s[r];s[r] c; l;r--;}} }

鸿蒙HarmonyOS应用开发:多码识别

华为HMS Core的扫码接口更适用于简单的个人使用。在商业环境&#xff0c;如货架、医用试管、图书馆书架等&#xff0c;常常遇到复杂的多码扫描需求&#xff0c;这时需要专业的扫码SDK。尽管当前市场上的主流商业SDK尚未支持鸿蒙HarmonyOS&#xff0c;但我们仍可以通过HTTP请求来…

「干货分享」如何使用CLion轻松解决C++开发者的7大痛点

CLion是一款专为开发C及C所设计的跨平台IDE&#xff0c;它是以IntelliJ为基础设计的&#xff0c;包含了许多智能功能来提高开发人员的生产力。这种强大的IDE帮助开发人员在Linux、OS X和Windows上来开发C/C&#xff0c;同时它还使用智能编辑器来提高代码质量、自动代码重构并且…

【UE】一个扫描效果(不使用后期处理体积)

效果 步骤 一、制作扫描效果 1. 在3dsmax中新建一个圆形 设置半径为50mm&#xff0c;勾选“自适应” 转换为可编辑多边形 导出 2. 打开虚幻编辑器&#xff0c;导入制作好的模型 3. 创建一个材质&#xff0c;这里命名为“M_Sphere” 打开“M_Sphere”&#xff0c;添加一个“V…

PCIe总线中Root Complex(RC)

在不同的处理器系统中&#xff0c;RC的实现有较大差异。PCIe总线规范并没有规定RC的实现细则。在有些处理器系统中&#xff0c;RC相当于PCIe主桥&#xff0c;也有的处理器系统也将PCIe主桥称为PCIe总线控制器。而在x86处理器系统中&#xff0c;RC除了包含PCIe总线控制器之外&am…

C语言:实现对单链表的反转 函数封装

需求&#xff1a; 实现对单链表的反转 代码实现&#xff1a; #include <stdio.h>typedef struct node{int data;struct node* next; }NODE;void PrintLink(NODE* phead) {NODE* p phead;while(p ! NULL){printf("%d ",p->data);p p->next;}printf(&…

好商品好内容好运营,图文免佣这些爆单技巧你会吗?

图文带货正在成为抖音电商商家们生意增长的新阵地。不同于平台传统的短视频、直播带货&#xff0c;图文体裁以其用户需求大、消费粘性高、深度互动强、正向反馈好的优势&#xff0c;令商家们看到生意新可能&#xff1b;而几张图背景音乐就能实现图文带货的便捷&#xff0c;也为…

瞬态抑制二极管TVS的核心参数?|深圳比创达电子EMC(上)

TVS二极管具有响应速度快、漏电流小、钳位电压稳以及无寿命衰减的特性&#xff0c;从小到信号线静电防护&#xff0c;大到电力系统抗雷击浪涌&#xff0c;TVS都发挥着至关重要的作用。本章对瞬态抑制二极管TVS核心参数展开分析&#xff0c;供产品选型参考。接下来就跟着深圳比创…

PCIe架构的处理器系统介绍

不同的处理器系统中&#xff0c;PCIe体系结构的实现方式不尽相同。PCIe体系结构以Intel的x86处理器为蓝本实现&#xff0c;已被深深地烙下x86处理器的印记。在PCIe总线规范中&#xff0c;有许多内容是x86处理器独有的&#xff0c;也仅在x86处理器的Chipset中存在。在PCIe总线规…

初识Node.js开发

一、Node.js是什么 1.node.js是什么 官方对Node.js的定义&#xff1a; Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。 也就是说Node.js基于V8引擎来执行JavaScript的代码&#xff0c;但是不仅仅只有V8引擎&#xff1a; 前面我们知道V8可以嵌入到任何C 应用…

保证跨境商城源码稳定性的重要性

稳定性保证源码的可靠性 在跨境电商领域&#xff0c;跨境商城源码的稳定性对于商城运营至关重要。一个稳定、可靠的源码能够确保商城的正常运行&#xff0c;提升用户体验&#xff0c;增加交易成功率&#xff0c;并为商城的可持续发展打下基础。 稳定性保证系统顺利运行 跨境商城…

第四个 1024,属于我们!

第四个 1024&#xff0c;只属于我们&#xff0c;愿人世间没有 BUG&#xff01; if (today1024) {printf("拒绝加班&#xff01;")} else {printf("没有Bug&#xff01;")}2023 - 1024 加油&#xff01; 我是故屿 一个在互联网苟且偷生的工具人 若非生…

内衣洗衣机便宜好用的牌子有哪些?四款公认好用洗内裤机推荐

在机器解放了双手的时代中&#xff0c;洗衣机走进了千家万户&#xff0c;虽然在某种程度上缓解了人们手洗衣服的压力&#xff0c;但还是有不少人选择了人工手洗自己的内衣内裤&#xff0c;甚至连袜子都是手工洗的&#xff0c;这让人很是郁闷&#xff0c;倒不是说洗衣机不方便&a…