Flutter 图片加载流程

642 阅读5分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

在 Flutter 中,图⽚的加载主要是通过 Image 控件实现的,⽽ Image 控件本身是⼀个

StatefulWidgetImage 有它的 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;

我们来逐步理解这个流程

image-20210301233459650

在 Flutter 的图⽚的加载流程中,主要有三个⻆⾊:

Image :⽤于显示图⽚的 Widget,最后通过内部的 RenderImage 绘制。

ImageProvider :提供加载图⽚的⽅式如 NetworkImage 、 FileImage 、 MemoryImage

、 AssetImage 等,从⽽获取 ImageStream ,⽤于监听结果。

ImageStream :图⽚的加载对象,通过 ImageStreamCompleter 最后会返回⼀个 ImageInfo

,⽽ ImageInfo 内包含有 RenderImage 最后的绘制对象 ui.Image

从上⾯的⼤图流程可知,⽹络图⽚是通过 NetworkImage 这个 Provider 去提供加载的,各类

Provider 的实现其实⼤同⼩异,其中主要需要实现的⽅法主要如下图所示:

image-20210301235209360

1obtainKey

该⽅法主要⽤于标示当前 Provider 的存在,⽐如在 NetworkImage 中,这个⽅法返回的是

SynchronousFuture(this) ,也就是 NetworkImage ⾃⼰本身,并且得到的这个

key 在 ImageProvider 中,是⽤于作为内存缓存的 key 值。

在 NetworkImage 中主要是通过 runtimeType 、 url 、 scale 这三个参数判断两

个 NetworkImage 是否相等,所以除了 url ,图⽚的 scale 同样会影响缓存的对象哦。

2load(T key)

load ⽅法顾名思义就是加载了,⽽该⽅法中所使⽤的 key ,毫⽆疑问就是上⾯ obtainKey ⽅法所

提供的。

load ⽅法返回的是 ImageStreamCompleter 抽象对象,它主要是⽤于管理和通知 ImageStream

中得到的 dart:ui.Image ,⽐如在 NetworkImage 中的是⼦类

MultiFrameImageStreamCompleter , 它可以处理多帧的动画,如果图⽚只有⼀针,那么将执⾏⼀次

都结束。

3resolve

ImageProvider 的关键在于 resolve ⽅法,从流程图我们可知,该⽅法在 Image 的⽣命周期回

调⽅法 didChangeDependencies 、 didUpdateWidget 、 reassemble ⾥会被调⽤,从下⽅源码

可以看出,上⾯我们所实现的 obtainKey 和 load 都会在这⾥被调⽤

image-20210301235239954

这个有个有意思的对象,就是 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 。

image-20210301235414995

发现没有,这⾥和我们理解上的 Cache 概念稍微有点不同,以前我们缓存的⼀般是 key - bitmap 对

象,也就是实际绘制数据,⽽在 Flutter 中,缓存的仅是 ImageStreamCompleter 对象,⽽不是实际

绘制对象 dart:ui.Image

3ImageStreamCompleter

ImageStreamCompleter 是⼀个抽象对象,它主要是⽤于管理和通知 ImageStream ,处理图⽚数

据后得到的包含有 dart:ui.Image 的对象 ImageInfo

接下来我们看 NetworkImage 中的 ImageStreamCompleter 实现类

MultiFrameImageStreamCompleter 。如下图代码所示, MultiFrameImageStreamCompleter 主要

通过 codec 参数获得渲染数据,⽽这个数据来源通过 _loadAsync ⽅法得到,该⽅法主要通过

http 下载图⽚后,对图⽚数据通过 PaintingBinding 进⾏ ImageCodec 编码处理,将图⽚转化为

引擎可绘制数据。

image-20210301235605573

⽽在 MultiFrameImageStreamCompleter 内部, ui.Codec 会被 ui.Image ,通过 ImageInfo

封装起来,并逐步往回回调到 _ImageState 中,然后通过 setState 将数据传递到

RenderImage 内部去绘制。

image-20210301235632927

其他

1、缓存数量

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

⽽在 Flutter 中, ImageCache 默认的缓存⼤⼩是

const int _kDefaultSize = 1000; const int _kDefaultSizeBytes = 100 << 20; // 100

所以简单粗暴的做法是: PaintingBinding.instance.imageCache.maximumSize = 100;

同时在⻚ ⾯不可⻅时暂停图⽚的加载等。