Flutter 图片加载方案分析之 Image
Flutter 默认提供了Image用于从网络、文件等加载图片,并且使用ImageCache统一管理图片缓存,但有时候并不能满足使用需求(比如网络图片没有磁盘缓存,导致每次 ImageCache 清除缓存之后又要从网络下载),所以又出现了flutter_cached_network_image、extended_image等基于 Flutter 原生的解决方案,以及power_image等基于混合开发的解决方案。
本文对 Flutter 中的 Image 加载过程、原理做一简单分析。
图片展示的流程
首先,简单梳理一下图片从加载到展示的过程。
Image
A widget that displays an image.
在查看 Image 具体实现之前,先了解几个基础方法:
ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size? size })
:创建 ImageConfiguration,一般用于state.didChangeDependencies
等依赖变化时会调用的地方,其创建的 ImageConfiguration 对象会传入BoxPainter.paint
或者ImageProvider.resolver
方法中以用来获取 ImageStream。Future<void> precacheImage(...)
预先加载 image 到 ImageCache 中,以便 Image、BoxDecoration、FadeInImage 等能够更快地加载 image。
Image 是 Flutter 中用于展示图片的 Widget,主要有如下用法:
- Image.new, for obtaining an image from an ImageProvider.
- Image.asset, for obtaining an image from an AssetBundle using a key.
- Image.network, for obtaining an image from a URL.
- Image.file, for obtaining an image from a File.
- Image.memory, for obtaining an image from a Uint8List.
支持的格式有:JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP,以及依赖于特定设备的格式(Flutter 会尝试使用平台 API 解析未知格式)。
通过指定cacheWidth
和cacheHeight
可以让引擎按照指定大小解码图片,可以降低 ImageCache 占用的内存。
Image(...)
构造函数中只有一个必传项ImageProvider image
用于获取图片,其余四种构造方法也都是在此方法的基础上分别指定了各自的 ImageProvider,以Image.network
为例:
Image.network(
String src, {
Key? key,
double scale = 1.0,
...,
int? cacheWidth,
int? cacheHeight,
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
...
super(key: key);
上述代码中的 ResizeImage,NetworkImage 等都继承自 ImageProvider,ImageProvider.resolve
方法创建并返回 ImageStream 供 Image 使用,内部通过ImageProvider.resolveStreamForKey
方法从 ImageCache 或者子类指定的途径(比如 NetworkImage 会从网络)加载图片(并保存到 ImageCache)。
_ImageState
Image是 StatefulWidget,处理 image 的主要逻辑在 _ImageState 中:其混入了WidgetsBindingObserver以便监听系统生命周期;在内部通过监听ImageStream获得ImageInfo并最终在_ImageState.build
方法中创建RawImage;RawImage 是一个LeafRenderObjectWidget,会创建RenderImage并在RenderImage.paint
根据之前获取的信息调用DecorationImagePainter.paintImage
方法通过canvas.drawImageRect
绘制图片。
_resolveImage
当依赖变化(didChangeDependencies()
)、Widget 变化(didUpdateWidget(Image oldWidget)
)、以及热更新(reassemble()
)时,_ImageState 会执行_resolveImage()
方法通过 ImageProvider 获取 ImageStream:
void _resolveImage() {
// ScrollAwareImageProvider 用于防止在快速滑动的时候加载图片
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
context: _scrollAwareContext,
imageProvider: widget.image,// 用户/构造方法指定的 ImageProvider
);
// 通过 ImageProvider 获取 ImageStream
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
assert(newStream != null);
_updateSourceStream(newStream);
}
我们还可以注意到,当在_resolveImage()
中获取到 ImageStream 之后,会通过_updateSourceStream()
更新 ImageStream。
_updateSourceStream
在此方法中,先是更新了ImageStream? _imageStream
对象,然后根据_isListeningToStream
的值执行_imageStream!.addListener(_getListener())
更新 ImageStream 的 Listener:
ImageStreamListener _getListener({bool recreateListener = false}) {
if(_imageStreamListener == null || recreateListener) {
_lastException = null;
_lastStack = null;
_imageStreamListener = ImageStreamListener(
_handleImageFrame,// 图片加载成功,使用获得的 imageInfo 更新 RawImage
onChunk: widget.loadingBuilder == null ? null : _handleImageChunk,// 展示 loading 动画
onError: widget.errorBuilder != null || kDebugMode
? (Object error, StackTrace? stackTrace) {
...
}
: null,// 展示加载失败
);
}
return _imageStreamListener!;
}
可以看到,在 ImageStreamListener 中,根据 ImageStream 的不同状态分别更新 Image 的显示。
_handleImageFrame
_handleImageFrame()
方法使用 ImageStream 中返回的ImageInfo,调用setState
方法更新_ImageState 中的ImageInfo? **_imageInfo**
属性,从而刷新 Image 展示。
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_replaceImage(info: imageInfo);// 在这里刷新 imageInfo,触发重建
_loadingProgress = null;
_lastException = null;
_lastStack = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
});
}
void _replaceImage({required ImageInfo? info}) {
_imageInfo?.dispose();
_imageInfo = info;
}
ImageInfo类内部持有ui.Image和其对应的scale
,以及一个获取图片像素大小的sizeBytes
方法。
// ImageInfo: a [dart:ui.Image] object with its corresponding scale.
ImageInfo({ required this.image, this.scale = 1.0, this.debugLabel })
int get sizeBytes => image.height * image.width * 4;
build
上面分析了_ImageState 如何监听使用 ImageProvider 获取到的 ImageStream,从中获取 ImageInfo 更新自己的 ImageInfo? _imageInfo
属性,那么这个属性是如何影响到我们的 Image 展示图片的呢,关键就在 build
方法中:
Widget build(BuildContext context) {
if (_lastException != null) {
if (widget.errorBuilder != null)
return widget.errorBuilder!(context, _lastException!, _lastStack);
if (kDebugMode)
return _debugBuildErrorWidget(context, _lastException!);
}
// 注意_ImageState 内部其实是使用 ui.Image _imageInfo?.image 创建了 RawImage 来展示图片
Widget result = RawImage(
// Do not clone the image, because RawImage is a stateless wrapper.
// The image will be disposed by this state object when it is not needed
// anymore, such as when it is unmounted or when the image stream pushes
// a new image.
image: _imageInfo?.image,// 这里会在 ImageStream 获取到 ImageInfo 之后更新
debugImageLabel: _imageInfo?.debugLabel,
width: widget.width,
height: widget.height,
scale: _imageInfo?.scale ?? 1.0,
color: widget.color,
opacity: widget.opacity,
colorBlendMode: widget.colorBlendMode,
fit: widget.fit,
alignment: widget.alignment,
repeat: widget.repeat,
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
invertColors: _invertColors,
isAntiAlias: widget.isAntiAlias,
filterQuality: widget.filterQuality,
);
if (!widget.excludeFromSemantics) {
result = Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel ?? '',
child: result,
);
}
if (widget.frameBuilder != null)
result = widget.frameBuilder!(context, result, _frameNumber, _wasSynchronouslyLoaded);
if (widget.loadingBuilder != null)
// 如果有 loadingBuilder 就包裹 result,所以注意进度为 100% 时要切换回图片,
// 否则会一直显示进度,而非加载的图片
result = widget.loadingBuilder!(context, result, _loadingProgress);
return result;
}
ImageInfo.image
是ui.Image对象,是原始的 image 像素,通过RawImage传入到Canvas.drawImageRect
或者Canvas.drawImageNine
绘制图片。
RawImage
A widget that displays a [dart:ui.Image]
directly.
RawImage 继承自 LeafRenderWidget,可以直接展示ui.Image
的内容,后者是解码的图片数据的不透明句柄(Opaque handle to raw decoded image data (pixels))、是对_Image
类的封装、对外提供宽高以及Image.toByteData
(将ui.Image
对象转化为ByteData
,ByteData 可以直接传入Canvas.drawImageRect
方法第一个参数)。
RawImage 主要逻辑就是创建/更新 RenderImage 的时候将从_ImageState.build
方法获得的ui.Image? image
的 clone 传入(其实就是使用ui.Image? image
对应的_Image _image
新建了一个 ui.Image,每一个 ui.Image 都是_image 的一个句柄,只有当没有 ui.Image 指向_image 时后者才会真正的 dispose)。
RenderImage
An image in the render tree.
RenderImage 作为一个 RenderBox,在从 RawImage 那里拿到ui.Image? _image
之后,然后在其RenderImage.paint
方法中,会调用paintImage
方法绘制_image
代表的图片。
ui.Image 实际是 ui._Image 的包装类,它的 width、height、toByteData 等方法最终都是调用 ui._Image 对应的实现。
paintImage
方法是位于 lib\src\painting\decoration_image.dart 的全局方法,在其内部调用 canvas 绘制_image 对应的图片。
至此,我们可以看到,在Image中,根据构造方法的不同创建了不同的ImageProvider对象作为Image.image
参数;
然后在 _ImageState 中,使用ImageProvider.resolve
方法创建并更新ImageStream? _imageStream
,并且监听ImageStream以便在图片加载成功之后获取ImageInfo? _imageInfo
;
这个ImageInfo是对ui.Image的封装类,在_ImageState.build
方法中被传入RawImage,后者则创建了RenderImage并最终将 ui.Image 的内容绘制在屏幕上面。
图片获取与缓存
到目前为止,我们大体梳理了图片展示的这部分流程,此外,还有一部分同样重要的流程——图片的获取与缓存。
ImageProvider
ImageProvider 是获取图片资源的基类,其他类可以调用ImageProvider.resolve
方法获取 ImageStream ,此方法会调用ImageCache.putIfAbsent
优先从 ImageCache 中获取,如果没有则调用ImageProvider.load
方法获取并缓存到 ImageCache 中。
其子类一般只需要重写ImageProvider
的ImageStreamCompleter load(T key, DecoderCallback decode)
和Future<T> obtainKey(ImageConfiguration configuration)
方法即可。
以NetworkImage
加载网络图片的过程为例:
我们通过
NetworkImage()
方法获取的实际是network_image.NetworkImage
对象。
当_ImageState._resolveImage()
方法调用ImageProvider.resolve
方法时,内部会调用ImageProvider.resolveStreamForKey
方法,在其内部会执行:
- 通过
ImageProvider.obtainKey
获取图片对应的 key - 执行
PaintingBinding.*instance*!.imageCache!.putIfAbsent(key,() =>
load(key, PaintingBinding.*instance*!.instantiateImageCodec),onError: handleError,)
方法,优先从 imageCache 中获取缓存的图片,没有的话执行ImageProvider.load
方法获取图片。
对于network_image.NetworkImage
对象,他的obtainKey()
和load()
方法实现如下:
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
const NetworkImage(this.url, { this.scale = 1.0, this.headers })
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
// 注意这里的 key 是 NetworkImage 对象,也就是说网络图片加载的 url,scale,
// header 等一致的话才会被认为命中缓存
return SynchronousFuture<NetworkImage>(this);
}
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key as NetworkImage, chunkEvents, decode),// 真正从网络加载图片的方法
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
informationCollector: () => <DiagnosticsNode>[
DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this),
DiagnosticsProperty<image_provider.NetworkImage>('Image key', key),
],
);
}
}
在这其中,network_image.NetworkImage._loadAsync()
方法才是真正使用HttpClient
从网上获取图片资源的方法(实际上 AssetBundleImageProvider、FileImage 和 MemoryImage 等一众 ImageProvider 等都约定俗成在_loadAsync
中执行真正获取图片的逻辑),返回值 Future<ui.Codec>和 ui.Image 的关系如下:
Future<ui.Image> decodeImageFromList(Uint8List bytes) async {
final ui.Codec codec = await PaintingBinding.instance.instantiateImageCodec(bytes);
final ui.FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
ImageCache
Class for caching images.
在上面方分析 ImageProvider 的时候,我们注意到,每次通过ImageProvider.resolveStreamForKey
方法获取 ImageStream 时,都会调用PaintingBinding.instance!.imageCache.putIfAbsent
方法优先获取Image 对象的缓存,这就涉及到和 Image 缓存有关的类——ImageCache。
ImageCache 对象全局唯一,使用 LRU 算法最多缓存1000 张或者最大 100MB 图片,可以分别使用maximumSize
和maximumSizeBytes
修改配置。
其内部维持有三个 Map:
Map<Object, PendingImage> _pendingImages
正在加载中的图片,可能可能同时也是_liveImage
(对应的 ImageStream 已经被监听了)。Map<Object, _CachedImage> _cache
缓存的图片,maximumSize 和 maximumSizeBytes 限制针对的是_cache
。Map<Object, _LiveImage> _liveImages
正在使用的图片,他的 ImageStreamCompleters 至少有一个 listener,可能同时在_pendingImages
(所以这里的_LiveImage 的sizeBytes
可能为 null)或者_liveImages
中。
_CachedImage
和_LiveImage
都继承自_CachedImageBase
,其内部持有ImageStreamCompleter
,图片的 handlerImageStreamCompleterHandle
,以及图片大小sizeBytes
。
ImageCacheStatus
处理 ImageCache 缓存的图片状态:
pending
,还没有加载完成的 image,如果被监听的话,还会是live
的keepAlive
,图片会被ImageCache._cache
保存。可能是 live 的,但不会 pending 的。live
,图片会一直被持有,除非ImageStreamCompleter
没有 listener 了。可能是 pending 的,也可能是 keepAlive 的untracked
,不会被缓存的图片(上述三值都为 false)。
可以使用ImageCache.statusForKey
或者ImageProvider.obtainCacheStatus
获取图片状态ImageCacheStatus
。
此外,ImageCache 还提供ImageCache.evict
方法从缓存中清除指定图片。
putIfAbsent
当 ImageProvider 调用ImageCache.putIfAbsent
方法获取 ImageStreamCompleter 时,会依次尝试从_pendingImages
、_cache
、_liveImages
中读取,如果都没有则会尝试执行传入的 loader 方法获取。
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
ImageStreamCompleter? result = _pendingImages[key]?.completer;
// Nothing needs to be done because the image hasn't loaded yet.
// 1. 如果图片还在加载中,就直接返回
if (result != null) {
return result;
}
// Remove the provider from the list so that we can move it to the
// recently used position below.
// Don't use _touch here, which would trigger a check on cache size that is
// not needed since this is just moving an existing cache entry to the head.
final _CachedImage? image = _cache.remove(key);
if (image != null) {
// The image might have been keptAlive but had no listeners (so not live).
// Make sure the cache starts tracking it as live again.
// 2. 如果_cache 中已经有了,就将其加入_liveImages 并返回
_trackLiveImage(
key,
image.completer,
image.sizeBytes,
);
_cache[key] = image;
return image.completer;
}
// 3. 如果_liveImages 中已经有了,而 cache 中没有,就加入_cache,
// 此时会检测大小和数量(这种属于图片刚下载完,或者已有缓存被清理)
final _LiveImage? liveImage = _liveImages[key];
if (liveImage != null) {
_touch(
key,
_CachedImage(
liveImage.completer,
sizeBytes: liveImage.sizeBytes,
),
timelineTask,
);
if (!kReleaseMode) {
timelineTask!.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
}
return liveImage.completer;
}
// 4.1 如果_pendingImages、_cacheImages、_liveImages 中都没有,就去下载
// 并加入到_liveImages 中,此时_LiveImage.sizeBytes 为 null
// 注意这里只是加入到_liveImages 中追踪,并未使用_cache,
// 故而也【不受其最大数量和最大总大小约束】
try {
result = loader();
_trackLiveImage(key, result, null);
} catch (error, stackTrace) {
...
}
// If we're doing tracing, we need to make sure that we don't try to finish
// the trace entry multiple times if we get re-entrant calls from a multi-
// frame provider here.
bool listenedOnce = false;
// We shouldn't use the _pendingImages map if the cache is disabled, but we
// will have to listen to the image at least once so we don't leak it in
// the live image tracking.
// If the cache is disabled, this variable will be set.
_PendingImage? untrackedPendingImage;
// 图片加载过程中的回调
void listener(ImageInfo? info, bool syncCall) {
int? sizeBytes;
if (info != null) {
sizeBytes = info.sizeBytes;
info.dispose();
}
final _CachedImage image = _CachedImage(
result!,
sizeBytes: sizeBytes,
);
_trackLiveImage(key, result, sizeBytes);
// Only touch if the cache was enabled when resolve was initially called.
// 4.2 图片加载成功,如果有缓存,就将此图片加入_cache 中,此时会检测大小和数量
// 并且这里的 sizeBytes 是图片实际大小
if (untrackedPendingImage == null) {
_touch(key, image, listenerTask);
} else {
image.dispose();
}
final _PendingImage? pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
listenedOnce = true;
}
final ImageStreamListener streamListener = ImageStreamListener(listener);
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, streamListener);
} else {
untrackedPendingImage = _PendingImage(result, streamListener);
}
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
return result;
}
经过上述分析可以知道,当_cache、_liveImages、_pendingImages 中都没有指定图片时,会从网络下载(或者磁盘、asset 等),而在图片完全加载完成之前,_pendingImages 中下载图片所占大小是没有被 ImageCache 追踪的,也就是说ImageCache._cache 的最大个数和总大小限制都不会管理这部分图片;故而面对大量高清大图加载的场景(比如,五列 1:1 网格加载平均大小几 Mb 的网络图片),如果快速滑动会导致_pendingImages 急速增大,这样下载中且还未完全下载的图片所占用的内存会逐渐累计,从而导致 Flutter APP内存暴增,页面卡顿等(本地资源不容易出现是因为从 load 到图片加载完成间隔比较短,而网络图片由于网速等导致_pendingImages 中会累计很多正在下载中的图片,会比较明显)。
那些在 Flutter 中加载图片并且完全采用 ImageCache 管理图片内存的图片加载框架比如 Image/ExtendedImage/CachedNetworkImage 等都存在此问题;阿里的 PowerImage 由于将图片下载这个过程交给了原生成熟的图片加载库处理,使得 ImageCache 只管理已经加载完成的图片,从而避免了上述情况。
可以看到,以从网络加载图片为例,Flutter 原生提供的 Image 只有内存中的 ImageCache 一级缓存,如果 ImageCache 没有指定的图片(首次加载或者缓存被清空)则会再次从网络加载,这会导致多图列表的时候图片被频繁的回收/重新下载,从而影响用户体验。
为了解决上述问题,涌现了很多第三方图片加载控件:
- extended_image 对官方 Image 的二次开发,增加了磁盘缓存。
- flutter_cached_network_image 使用sqflite 数据库管理缓存的网络图片加载库,增加了磁盘缓存。
- power_image 使用于混合项目的图片加载库,提供ffi和texture两种图片展示方式,依赖于原生图片加载库(比如Glide)加载图片、管理缓存。
总结
简单总结一下 Flutter 原生 Image 组件加载图片的流程:
简单来说如下:
- 用户通过 Image Widget 的各个构造方法创建指定的 ImageProvider;
- 在_ImageState 中使用
ImageProvider.resolve(ImageConfiguration)
获取并监听 ImageStream(listener 为 ImageStreamListener); - ImageProvider 会按照传入的 ImageConfiguration 生成的 key 在 ImageCache 中查找对应的缓存,没有的话则先加载再缓存;
- 当 ImageProvider 成功加载图片时,ImageStreamListener 获得 ImageInfo 时,并触发
_ImageState.build()
方法将ui.Image _imageInfo?.image
传入 RawImage 中; - 作为一个 LeafRenderObjectWidget,RawImage 创建 RenderImage 并传入
ui.Image? image?.clone()
作为RenderImage.image
,此后再在RenderImage.paint
方法中调用系统的paintImage()
方法通过canvas.drawImageRect
绘制图片内容。