Flutter 之 precacheImage() 浅析

Flutter 中 precacheImage() 方法是如何提高图片加载速度的?

Flutter 文档中提到调用 precacheImage() 预先加载图片加载到缓存,后续如果这张图片被 Image 控件用到的话,加载起来会更快。

Prefetches an image into the image cache.

If the image is later used by an [Image] or [BoxDecoration] or [FadeInImage], it will probably be loaded faster.

precacheImage() 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
Future<void> precacheImage(
ImageProvider provider,
BuildContext context, {
Size size,
ImageErrorListener onError,
}) {
final ImageConfiguration config = ...;
final Completer<void> completer = Completer<void>();
final ImageStream stream = provider.resolve(config);
...
return completer.future;
}

precacheImage() 是如何加快图片加载速度的?我们可以从如下两个角度来考虑这个问题:

  • 写缓存 - 如何将图片加入缓存
  • 读缓存 - 如何从缓存取出图片

写缓存

先上结论:precacheImage() 调用 ImageProvider.resolve() 从缓存中取出图片

precacheImage() 主要的调用关系如下:

1
2
3
4
precacheImage() ->
ImageProvider.resolve() ->
ImageCache.putIfAbsent() ->
ImageProvider.load() ->

ImageProvider.resolve()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 ImageStream resolve(ImageConfiguration configuration) {
...;
final ImageStream stream = ImageStream();
T obtainedKey;

key = obtainKey(configuration);
key.then<void>((T key) {
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}).catchError(handleError);
return stream;
}

注意 imageCache.putIfAbsent() 第二个参数形式如下:

1
2
3
4
ImageStreamCompleter putIfAbsent(
Object key,
ImageStreamCompleter loader(),
{ ImageErrorListener onError })

所以这里的 loader 参数实际上是对 load() 方法的封装。

ImageCache.putIfAbsent()

ImageCache 用于缓存图片的类。这个类实现了 LRU 算法,最多可以保存1000个图片或者100MB。

1
2
3
class ImageCache {
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
  • _pendingImages - 保存正在加载中的图片
  • _cache - 保存已加载到缓存的图片

putIfAbsent() 方法是 ImageCache 主要的入口方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
ImageStreamCompleter result = _pendingImages[key]?.completer;
// Nothing needs to be done because the image hasn't loaded yet.
if (result != null)
return result;
// Remove the provider from the list so that we can move it to the
// recently used position below.
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
try {
result = loader();
} catch (error, stackTrace) {
...
}
void listener(ImageInfo info, bool syncCall) {
// Images that fail to load don't contribute to cache size.
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize);
...
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}

_cache[key] = image;
...
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
}
return result;
}

其过程总结如下:

  • 如果 _pendingImages 存在对应的 key,说明正在加载该图片,此时什么也不用做所以直接返回
  • 如果 _cache 存在对应的 key,说明已经加载过该图片,此时将图片调整到 LRU 位置并返回
  • 否则,调用 loader() 来加载图片。loader() 的返回值为 result
    • result 创建新对象并保存到 _pendingImages
    • 监听图片加载过程。加载成功后从 _pendingImages 移除对应的 key,并在 _cache 中保存加载结果

ImageProvider.load()

ImageProvider 是一个抽象类,它的 load()obtainKey() 是抽象方法,需要 ImageProvider 的子类实现。

AssetBundleImageProvider 为例,load() 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKey> {
@override
ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<AssetBundleImageKey>('Image key', key);
},
);
}

@protected
Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
final ByteData data = await key.bundle.load(key.name);
if (data == null)
throw 'Unable to read data';
return await decode(data.buffer.asUint8List());
}
}

从代码不难看出这里的 _loadAsync() 即底层的 asset 图片加载过程,它由 load() 方法调用。而 load() 是前面 result = loader(); 的具体执行过程。

至此可知,调用 precache() 可将指定 key 对应的 _CachedImage 保存到 ImageCache 中。

读缓存

先上结论:Image 调用 ImageProvider.resolve() 从缓存中取出图片

Image 控件用于显示图片。有多种方式显示图片:

  • Image.asset - 使用 key 从 AssetBundle 图片
  • Image.network() - 使用 url 从网络加载图片
  • Image.file - 从文件加载图片
  • Image.memory - 从内存加载图片

这些显示方式都有各自的 ImageProvider

  • AssetImage
  • NetworkImage
  • FileImage
  • MemoryImage

上述这些 Image 都继承自 ImageProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class ImageProvider<T> {
ImageStream resolve(ImageConfiguration configuration) {}
Future<bool> evict({ ImageCache cache, ImageConfiguration configuration = ImageConfiguration.empty }) async {}
Future<T> obtainKey(ImageConfiguration configuration);
ImageStreamCompleter load(T key, DecoderCallback decode);
}

abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKey> {}

abstract class NetworkImage extends ImageProvider<NetworkImage> {

class FileImage extends ImageProvider<FileImage> {

class MemoryImage extends ImageProvider<MemoryImage> {}

Image 它是一个 StatefulWidget,它依赖于其 ImageProvider 成员,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Image extends StatefulWidget {
final ImageProvider image;
_ImageState createState() => _ImageState();
}

class _ImageState extends State<Image> with WidgetsBindingObserver {
@override
void didChangeDependencies() {
...
_resolveImage();
...
super.didChangeDependencies();
}

@override
void didUpdateWidget(Image oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.image != oldWidget.image)
_resolveImage();
}

void _resolveImage() {
final ImageStream newStream =
widget.image.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
_updateSourceStream(newStream);
}
}

注意这里的 _resolveImage() 方法,它调用 ImageProvider.resolve()。所以可以认为是由 ImageProvider.resolve() 从缓存中取出图片(前面分析过 ImageProvider.resolve() 与图片缓存的关系)。

总结

precacheImage() 提升图片加载速度的关键其实在于主动调用 ImageProvider.resolve() 来提前将图片加载到 ImageCache,示意图如下(这里假设从本地读取图片):

-w850

如果后续有 Image 显示同一个 key 对应的图片,再次调用 ImageProvider.resolve() 的速度将大为提升(网络图片会尤其明显),当然前提是该图片仍然在图片缓存中。

参考