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; if (result != null ) return result; 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) { 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); 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
,示意图如下(这里假设从本地读取图片):
如果后续有 Image
显示同一个 key 对应的图片,再次调用 ImageProvider.resolve()
的速度将大为提升(网络图片会尤其明显),当然前提是该图片仍然在图片缓存中。
参考