Flutter 中提供了Widget 组件供开发者解决日常中关于图片相关的需求,其中包含 Image.file() 读取用户内存中图片,Image.asset() 加载程序包含的图片资源,Image.network() 用于网络图片的加载,我们主要通过 Image 对于网络图片加载的实现来初步了解 Image 组件的原理。
首先从构造函数开始:
Image.network(
String src, {
Key? key,
double scale = 1.0,
...
}) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
...
super(key: key);
/// The image to display.
final ImageProvider image;
其中在构造Image对象时,最重要的变量 image,是一个ImageProvider 对象,从名字可以看出他是图片的提供者,ImageProvider 是一个抽象类,子类包括 NetworkImage,FileImage,AssetImage,MemoryImage等,稍后我们细说NetworkImage,继续看 Image 组件的实现。
Image 作为一个 StatefulWidget 他的状态由 _ImageState 控制,从生命周期的执行顺序看下来
@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream(keepStreamAlive: true);
super.didChangeDependencies();
}
void _resolveImage() {
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
context: _scrollAwareContext,
imageProvider: widget.image,
);
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);
}
函数通过调用 imageProvider 的 resolve 方法创建 ImageStream 对象,该对象是一个图片资源的句柄,它持有图片资源加载完毕后的监听回调和图片资源的管理者,而后续介绍到其中的 ImageStreamCompleter 对象是图片资源的一个管理类; resolve 方法是 ImageProvider 暴露给 Image 组件的主入口方法,接受一个图片相关的配置对象,返回对应的 ImageStream 图片数据流,我们深入查看 NetwokImage 的 resolve 实现。
@nonVirtual
ImageStream resolve(ImageConfiguration configuration) {
assert(configuration != null);
final ImageStream stream = createStream(configuration);
// 加载key(可能是异步的),设置错误处理zone,然后调用 resolveStreamForKey。
_createErrorHandlerAndKey(
configuration,
(T key, ImageErrorListener errorHandler) {
resolveStreamForKey(configuration, stream, key, errorHandler);
},
(T? key, Object exception, StackTrace? stack) async {
...
},
);
return stream;
}
void _createErrorHandlerAndKey(
ImageConfiguration configuration,
_KeyAndErrorHandlerCallback<T> successCallback,
_AsyncKeyErrorHandler<T?> errorCallback,
) {
T? obtainedKey;
bool didError = false;
Future<void> handleError(Object exception, StackTrace? stack) async {
...
}
// 创建一个新Zone,主要是为了当发生错误时不会干扰MainZone
final Zone dangerZone = Zone.current.fork(...);
dangerZone.runGuarded(() {
Future<T> key;
try {
// 生成缓存key,后面会根据此key来检测是否有缓存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then<void>((T key) {
obtainedKey = key;
try {
// 成功回调
successCallback(key, handleError);
} catch (error, stackTrace) {
handleError(error, stackTrace);
}
}).catchError(handleError);
});
}
@protected
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
...
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent(
key,
() => load(key, PaintingBinding.instance!.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
stream.setCompleter(completer);
}
}
ImageStream 中的图片管理者 ImageStreamCompleter 通过 PaintingBinding.instace.imageCache.putIfAbsent(key, ()=> load(), onError: handleError) 方法创建,ImageCache 是Flutter 框架中用于图片缓存的对象, 全局单例;
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) {
...
ImageStreamCompleter? result = _pendingImages[key]?.completer;
if (result != null) {
...
return result;
}
// 先移除缓存,后再添加,可以让最新使用过的缓存在_map中的位置更近一些,清理时会LRU来清除
final _CachedImage? image = _cache.remove(key);
if (image != null) {
...
_trackLiveImage(
key,
image.completer,
image.sizeBytes,
);
_cache[key] = image;
return image.completer;
}
final _LiveImage? liveImage = _liveImages[key];
if (liveImage != null) {
_touch(
key,
_CachedImage(
liveImage.completer,
sizeBytes: liveImage.sizeBytes,
),
timelineTask,
);
...
return liveImage.completer;
}
try {
result = loader();
_trackLiveImage(key, result, null);
} catch (error, stackTrace) {
...
}
bool listenedOnce = false;
_PendingImage? untrackedPendingImage;
void listener(ImageInfo? info, bool syncCall) {
int? sizeBytes;
if (info != null) {
sizeBytes = info.image.height * info.image.width * 4;
info.dispose();
}
final _CachedImage image = _CachedImage(
result!,
sizeBytes: sizeBytes,
);
_trackLiveImage(key, result, 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;
}
通过以上代码可以看到会通过key来查找缓存中是否存在,如果存在则返回,如果不存在则会通过执行loader()方法创建图片资源管理者,这里的 loader() 方法为入参 imageProvider 的 load() 方法,而后再将缓存图片资源的监听方法注册到新建的图片管理者中以便图片加载完毕后做缓存处理。
而ImageProvider 的 load 是加载图片数据源的方法,不同的数据源加载方法不同,由具体子类实现, 查看相关 NetworkImage 的实现。
@override
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) {
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: () {
...
},
);
}
返回值为 ImageStreamCompleter ,抽象类,定义了管理图片加载过程的一些接口,Image 组件正是通过它来监听图片加载状态的;MultiFrameImageStreamCompleter
继承自 ImageStreamCompleter
,他是一个多帧图片管理器,所以说 Flutter 是支持 GIF 图片的。
MultiFrameImageStreamCompleter
需要一个codec
参数,通过查看具体的代码可以看出图片的编解码不在Dart中完成,而是通过 flutter engine 实现,但是通过注释可以了解到具体的功能。
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
@pragma('vm:entry-point')
Codec._();
int? _cachedFrameCount;
/// 图片中的帧数(动态图会有多帧)
int get frameCount => _cachedFrameCount ??= _frameCount;
int get _frameCount native 'Codec_frameCount';
int? _cachedRepetitionCount;
/// 动画重复的次数
/// * 0 表示只执行一次
/// * -1 表示循环执行
int get repetitionCount => _cachedRepetitionCount ??= _repetitionCount;
int get _repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future<FrameInfo> getNextFrame() async {
final Completer<FrameInfo> completer = Completer<FrameInfo>.sync();
final String? error = _getNextFrame((_Image? image, int durationMilliseconds) {
if (image == null) {
completer.completeError(Exception('Codec failed to produce an image, possibly due to invalid image data.'));
} else {
completer.complete(FrameInfo._(
image: Image._(image),
duration: Duration(milliseconds: durationMilliseconds),
));
}
});
if (error != null) {
throw Exception(error);
}
return completer.future;
}
/// 失败时返回错误消息,成功时返回 null
String? _getNextFrame(void Function(_Image?, int) callback) native 'Codec_getNextFrame';
/// 释放此对象使用的资源。调用此方法后,该对象不再可用。
void dispose() native 'Codec_dispose';
}
创建对象时的codec 变量由 _loadAsync 方法的返回值初始化,查看该方法的实现
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
image_provider.DecoderCallback decode,
) async {
try {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
// 设置用户自定义的 header
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
// 请求失败抛出异常
await response.drain<List<int>>();
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int? total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return decode(bytes);
} catch (e) {
scheduleMicrotask(() {
PaintingBinding.instance!.imageCache!.evict(key);
});
rethrow;
} finally {
chunkEvents.close();
}
}
可以看到_loadAsync
方法主要做了两件事:
1.下载图片。
2. 对下载的图片数据进行解码。
在图片下载完成后,调用 decode 对图片二进制数据进行解码得到图片编解码器对象 codec;至此图片加载所需要的对象基本初始化完成,Image 得到了ImageStream,继续执行接下来的步骤:
void _updateSourceStream(ImageStream newStream) {
if (_imageStream?.key == newStream.key)
return;
if (_isListeningToStream)
_imageStream!.removeListener(_getListener());
if (!widget.gaplessPlayback)
setState(() { _replaceImage(info: null); });
setState(() {
_loadingProgress = null;
_frameNumber = null;
_wasSynchronouslyLoaded = false;
});
_imageStream = newStream;
if (_isListeningToStream)
_imageStream!.addListener(_getListener());
}
会给 imageStream 添加监听器,而我们之前创建的 MultiFrameImageStream 的添加监听:
@override
void addListener(ImageStreamListener listener) {
if (!hasListeners && _codec != null)
_decodeNextFrameAndSchedule();
super.addListener(listener);
}
Future<void> _decodeNextFrameAndSchedule() async {
_nextFrame?.image.dispose();
_nextFrame = null;
try {
_nextFrame = await _codec!.getNextFrame();
} catch (exception, stack) {
...
return;
}
if (_codec!.frameCount == 1) {
if (!hasListeners) {
return;
}
_emitFrame(ImageInfo(
image: _nextFrame!.image.clone(),
scale: _scale,
debugLabel: debugLabel,
));
_nextFrame!.image.dispose();
_nextFrame = null;
return;
}
_scheduleAppFrame();
}
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}
如果是静态图片,通过 _emitFrame 将图片资源提交出去,对于多帧的动画gif,会在监听到屏幕刷新时获取下一帧数据进行渲染;
而 setImage 会最终遍历这个 ImageStream 的监听者,触发对应的回调;
@protected
void setImage(ImageInfo image) {
...
final List<ImageStreamListener> localListeners =
List<ImageStreamListener>.from(_listeners);
for (final ImageStreamListener listener in localListeners) {
try {
listener.onImage(image.clone(), false);
} catch (exception, stack) {
...
}
}
}
最终 Image 组件的回调为 _handleImageFrame
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_replaceImage(info: imageInfo);
_loadingProgress = null;
_lastException = null;
_lastStack = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
});
}
流程图大概如下:
后续渲染过程就不再细说了,补充说明一下 ImageProvider 的 obtainKey 只做了声明,需要由子类做具体实现,而生成的 key 的主要作用在于 ImageCache 做缓存时 Map 保存数据时的 键值;
而Map中在判断 key 是否相等时会使用“==”运算符,所以对于NetworkImage 而言,只要 url 或者 缩放比例 不同就会重新加载图片。
@override
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
return other is NetworkImage
&& other.url == url
&& other.scale == scale;
}
顺便查看 ImageCache 关于清理缓存的策略,遵循了LRU (least-recently-used) 原则,会将最先加入的数据优先清理出Map。
const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
// 正在加载中的图片队列
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
// 缓存队列
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
// 展示中
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
// 当缓存数量超过最大值或缓存的大小超过最大缓存容量,会调用此方法清理到缓存上限以内
void _checkCacheSize(TimelineTask? timelineTask) {
...
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key]!;
_currentSizeBytes -= image.sizeBytes!;
image.dispose();
_cache.remove(key);
}
...
}
可以看出 image_cache 主要使用三个 Map 装载 展示中 / 加载中 / 缓存 的图片,对于缓存的管理遵循 LRU思想,清理的数据总是 Map 中最先加入的数据。
evict 主要根据提供的key,将缓存数据清除出Map。
// 清除指定key对应的图片缓存
bool evict(Object key, { bool includeLive = true }) {
assert(includeLive != null);
if (includeLive) {
final _LiveImage? image = _liveImages.remove(key);
image?.dispose();
}
final _PendingImage? pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
...
pendingImage.removeListener();
return true;
}
final _CachedImage? image = _cache.remove(key);
if (image != null) {
...
// 释放图片后重新计算当前缓存文件的大小
_currentSizeBytes -= image.sizeBytes!;
image.dispose();
return true;
}
return false;
}
总结
图片缓存是在内存中,并没有进行本地文件持久化存储,这也是为什么网络图片在应用重启后需要重新联网下载的原因。同时也意味着在应用生命周期内,如果缓存没有超过上限,相同的图片只会被下载一次。