在 Flutter 中,图片的加载主要是通过 控件实现的,而 Image 控件本身是一个 StatefulWidget ,通过前文我们可以快速想到, Image 肯定对应有它的 RenderObject 负责 layoutpaint ,那么这个过程中,图片是如何变成画面显示出来的?

Flutter 的图片加载流程其实“并不复杂”,具体可点击下方大图查看,以网络图片加载为例子,先简单总结,其中主要流程是:

  • 1、首先 Image 通过 ImageProvider 得到 ImageStream 对象
  • 2、然后 _ImageState 利用 ImageStream 添加监听,等待图片数据
  • 3、接着 ImageProvider 通过 load 方法去加载并返回 ImageStreamCompleter 对象
  • 4、然后 ImageStream 会关联 ImageStreamCompleter
  • 5、之后 ImageStreamCompleter 会通过 http 下载图片,再经过 PaintingBinding 编码转化后,得到 ui.Codec 可绘制对象,并封装成 ImageInfo 返回
  • 6、接着 ImageInfo 回调到 ImageStream 的监听,设置给 _ImageState build 的 RawImage 对象。
  • 7、最后 RawImageRenderImage 通过 paint 绘制 ImageInfo 中的 ui.Codec

是不是感觉有点晕了?relax!后面我们将逐步理解这个流程。

在 Flutter 的图片的加载流程中,主要有三个角色:

  • Image :用于显示图片的 Widget,最后通过内部的 RenderImage 绘制
  • ImageStream:图片的加载对象,通过 ImageStreamCompleter 最后会返回一个 ImageInfo ,而 ImageInfo 内包含有 最后的绘制对象 ui.Image

从上面的大图流程可知,网络图片是通过 NetworkImage 这个 Provider 去提供加载的,各类 Provider 的实现其实大同小异,其中主要需要实现的方法主要如下图所示:

十、 深入图片加载流程 - 图1

1、obtainKey

该方法主要用于标示当前 Provider 的存在,比如在 NetworkImage 中,这个方法返回的是 SynchronousFuture<NetworkImage>(this),也就是 NetworkImage 自己本身,并且得到的这个 key 在 ImageProvider 中,是用于作为内存缓存的 key 值

NetworkImage 中主要是通过 runtimeTypeurlscale 这三个参数判断两个NetworkImage 是否相等,所以除了 url ,图片的 scale 同样会影响缓存的对象哦。

2、load(T key)

load 方法顾名思义就是加载了,而该方法中所使用的 key ,毫无疑问就是上面 obtainKey 方法所提供的。

3、resolve

ImageProvider 的关键在于 resolve 方法,从流程图我们可知,该方法在 Image 的生命周期回调方法 didChangeDependenciesdidUpdateWidgetreassemble 里会被调用,从下方源码可以看出,上面我们所实现的 obtainKeyload 都会在这里被调用

resolve 方法内主要是用到了 PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)PaintingBinding 是一个胶水类,主要是通过 Mixins 粘在 WidgetsFlutterBinding 上使用,而以前的篇章我们说过, WidgetsFlutterBinding 就是我们的启动方法 的执行者。

所以图片缓存是在PaintingBinding.instance.imageCache内单例维护的。

如下图所示,putIfAbsent 方法内部,主要是通过 key 判断内存中是否已有缓存、或者正在缓存的对象,如果是就返回该 ImageStreamCompleter ,不然就调用 loader 去加载并返回。

值得注意的是,此时的的 cache 是有两个状态的,因为返回的 ImageStreamCompleter 并不代表着图片就加载完成,所以如果是首次加载,会先有 _PendingImage 用于标示该key的图片处于加载中的状态 ,并且添加一个 listener, 用于图片加载完成后,替换为缓存 _CacheImage

十、 深入图片加载流程 - 图2

发现没有,这里和我们理解上的 Cache 概念稍微有点不同,以前我们缓存的一般是 key - bitmap 对象,也就是实际绘制数据,而在 Flutter 中,缓存的仅是ImageStreamCompleter 对象,而不是实际绘制对象 dart:ui.Image

3、ImageStreamCompleter

ImageStreamCompleter 是一个抽象对象,它主要是用于管理和通知 ImageStream ,处理图片数据后得到的包含有 dart:ui.Image 的对象 ImageInfo

接下来我们看 NetworkImage 中的 ImageStreamCompleter 实现类 MultiFrameImageStreamCompleter 。如下图代码所示,MultiFrameImageStreamCompleter 主要通过 codec 参数获得渲染数据,而这个数据来源通过 _loadAsync 方法得到,该方法主要通过 http 下载图片后,对图片数据通过 PaintingBinding 进行 ImageCodec 编码处理,将图片转化为引擎可绘制数据。

而在 MultiFrameImageStreamCompleter 内部, ui.Codec 会被 ui.Image ,通过 ImageInfo 封装起来,并逐步往回回调到 _ImageState 中,然后通过 setState 将数据传递到 RenderImage 内部去绘制。

怎么样,现在再回过头去看开头的流程图,有没有一切明了的感觉?

通过上方流程的了解,我们知道 Flutter 实现了图片的内存缓存,但是并没有实现图片的本地缓存,所以我们入手的点,应该从 ImageProvider 开始。

通过上面对 NetworkImage 的分析,我们知道图片是在 _loadAsync 方法通过 http 下载的,所以最简单的就是,我们从 NetworkImage cv 一份代码,修改 _loadAsync 支持 http 下载前读取本地缓存,下载后通过将数据保存在本地。

结合 flutter_cache_manager 插件,如下方代码所示,就可以快速简单实现图片的本地缓存:

1、缓存数量

在闲鱼关于 Flutter 线上应用的内存分析文章中,有过对图片加载对内存问题的详细分析,其中就有一个是 ImageCache 的问题。

上面的流程我们知道, ImageCache 缓存的是一个异步对象,缓存异步加载对象的一个问题是,在图片加载解码完成之前,你无法知道到底将要消耗多少内存,并且大量的图片加载,会导致的解码任务需要产生大量的IO。

而在 Flutter 中, ImageCache 默认的缓存大小是

  1. const int _kDefaultSize = 1000;
  2. const int _kDefaultSizeBytes = 100 << 20; // 100

所以简单粗暴的做法是: 同时在页面不可见时暂停图片的加载等。

2、.9图

在 Image中,可以通过 centerSlice 配置参数设置.9图效果哦。

完整开源项目推荐:

我们还会再见吗?