这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
在 Flutter 中,图⽚的加载主要是通过 Image 控件实现的,⽽ Image 控件本身是⼀个
StatefulWidget , Image 有它的 RenderObject 负责 layout
和 paint ,那么这个过程中,图⽚是如何变成画⾯显示出来的?
⼀、图⽚流程
Flutter 的图⽚加载流程其实“并不复杂”,具体可点击下⽅⼤图查看,以⽹络图⽚加载为例⼦,先简单总
结,其中主要流程是:
1、⾸先 Image 通过 ImageProvider 得到 ImageProvider 对象
2、然后 _ImageState 利⽤ ImageStream 添加监听,等待图⽚数据
3、接着 ImageProvider 通过 load ⽅法去加载并返回 ImageStreamCompleter 对象
4、然后 ImageStream 会关联 ImageStreamCompleter
5、之后 ImageStreamCompleter 会通过 http 下载图⽚,再经过 PaintingBinding 编码转化
后,得到 ui.Codec 可绘制对象,并封装成 ImageInfo 返回
6、接着 ImageInfo 回调到 ImageStream 的监听,设置给 _ImageState build 的
RawImage 对象。
7、最后 RawImage 的 RenderImage 通过 paint 绘制 ImageInfo 中的 ui.Codec
注意,这的 ui.Codec 和后⾯的 ui.Image 等,只是因为 Flutter 中在导⼊对象时,为了和其
他类型区分⽽加⼊的重命名: import 'dart:ui' as ui show Codec;
我们来逐步理解这个流程
在 Flutter 的图⽚的加载流程中,主要有三个⻆⾊:
Image :⽤于显示图⽚的 Widget,最后通过内部的 RenderImage 绘制。
ImageProvider :提供加载图⽚的⽅式如 NetworkImage 、 FileImage 、 MemoryImage
、 AssetImage 等,从⽽获取 ImageStream ,⽤于监听结果。
ImageStream :图⽚的加载对象,通过 ImageStreamCompleter 最后会返回⼀个 ImageInfo
,⽽ ImageInfo 内包含有 RenderImage 最后的绘制对象 ui.Image 。
从上⾯的⼤图流程可知,⽹络图⽚是通过 NetworkImage 这个 Provider 去提供加载的,各类
Provider 的实现其实⼤同⼩异,其中主要需要实现的⽅法主要如下图所示:
1、obtainKey
该⽅法主要⽤于标示当前 Provider 的存在,⽐如在 NetworkImage 中,这个⽅法返回的是
SynchronousFuture(this) ,也就是 NetworkImage ⾃⼰本身,并且得到的这个
key 在 ImageProvider 中,是⽤于作为内存缓存的 key 值。
在 NetworkImage 中主要是通过 runtimeType 、 url 、 scale 这三个参数判断两
个 NetworkImage 是否相等,所以除了 url ,图⽚的 scale 同样会影响缓存的对象哦。
2、load(T key)
load ⽅法顾名思义就是加载了,⽽该⽅法中所使⽤的 key ,毫⽆疑问就是上⾯ obtainKey ⽅法所
提供的。
load ⽅法返回的是 ImageStreamCompleter 抽象对象,它主要是⽤于管理和通知 ImageStream
中得到的 dart:ui.Image ,⽐如在 NetworkImage 中的是⼦类
MultiFrameImageStreamCompleter , 它可以处理多帧的动画,如果图⽚只有⼀针,那么将执⾏⼀次
都结束。
3、resolve
ImageProvider 的关键在于 resolve ⽅法,从流程图我们可知,该⽅法在 Image 的⽣命周期回
调⽅法 didChangeDependencies 、 didUpdateWidget 、 reassemble ⾥会被调⽤,从下⽅源码
可以看出,上⾯我们所实现的 obtainKey 和 load 都会在这⾥被调⽤
这个有个有意思的对象,就是 Zone !
因为在 Flutter 中,同步异常可以通过try-catch捕获,⽽异步异常如 Future ,是⽆法被当前的
try-catch 直接捕获的。
所以在 Dart中 Zone 的概念,你可以给执⾏对象指定⼀个 Zone ,类似提供⼀个沙箱环境,⽽
在这个沙箱内,你就可以全部可以捕获、拦截或修改⼀些代码⾏为,⽐如所有未被处理的异常。
resolve ⽅法内主要是⽤到了 PaintingBinding.instance.imageCache.putIfAbsent(key, () =>
load(key) , PaintingBinding 是⼀个胶⽔类,主要是通过 Mixins 粘在
WidgetsFlutterBinding 上使⽤。
所以图⽚缓存是在PaintingBinding.instance.imageCache内单例维护的。
如下图所示, putIfAbsent ⽅法内部,主要是通过 key 判断内存中是否已有缓存、或者正在缓存
的对象,如果是就返回该 ImageStreamCompleter ,不然就调⽤ loader 去加载并返回。
值得注意的是,此时的的 cache 是有两个状态的,因为返回的 ImageStreamCompleter 并不代表着
图⽚就加载完成,所以如果是⾸次加载,会先有 _PendingImage ⽤于标示该key的图⽚处于加载中
的状态 ,并且添加⼀个 listener , ⽤于图⽚加载完成后,替换为缓存 _CacheImage 。
发现没有,这⾥和我们理解上的 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 内部去绘制。
其他
1、缓存数量
上⾯的流程我们知道, ImageCache 缓存的是⼀个异步对象,缓存异步加载对象的⼀个问题是,在图⽚加载解码完成之前,你⽆法知道到底将要消耗多少内存,并且⼤量的图⽚加载,会导致的解码任务需要产⽣⼤量的IO。
⽽在 Flutter 中, ImageCache 默认的缓存⼤⼩是
const int _kDefaultSize = 1000; const int _kDefaultSizeBytes = 100 << 20; // 100
所以简单粗暴的做法是: PaintingBinding.instance.imageCache.maximumSize = 100;
同时在⻚ ⾯不可⻅时暂停图⽚的加载等。