前言
Flutter的Image组件由四种加载形式:network、file、asset、memory,四种方式分别为网络、文件格式、asset资源、Uint8List格式数据。下面以通过Image.network()形式深入了解Flutter如何实现对图片加载和渲染。
提前知道点什么
ImageProvider
它的主要职责是提供数据资源、图片缓存。ResizeImage、NetworkImage、FileImage等都是对它的具体实现,根据不同数据源在load()实现图片加载方式。
- resolve(返回ImageStream监听图片加载过程)
- evict(图片缓存相关)
- load(图片加载方法,由子类具体实现)
PaintingBinding
PaintingBinding是全局单例,包含了ImageCache成员对象。在ImageProvider的resolve方法中如果对于图片对象key存在缓存会从ImageCache中直接读取对象;在获取图片资源Uint8List数据后通过instantiateImageCodec创建编解码器。所以PaintingBinding在图片加载过程中主要提供图片缓存和图片资源编解码的作用。
- instantiateImageCodec
- PaintingBinding.instance.imageCache
MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter负责chunkEvents(资源加载情况)和FrameInfo(图片帧)。如果是gif动图调用_scheduleAppFrame()循环获取_nextFrame.image绘制。使用_emitFrame()将ImageInfo通过ImageStream注册的ImageStreamListener监听接口传递到State。
- _handleCodecReady
- _decodeNextFrameAndSchedule
- _handleAppFrame(gif图片调用)
Codec
Codec是图片编解码句柄,主要提供了FlutterEngine底层获取图片帧数据接口。所以这里实现图片编解码的过程并非在Dart层级。
- getNextFrame(获取图片帧)
- instantiateImageCodec(获取解码器)
网络图片加载过程
-
网络图片加载过程是在NetwrokImage调用load()方法时实例化MultiFrameImageStreamCompleter通过调用_loadAsync()方法。
-
_loadAsync()通过静态HttpClient通过图片地址Url下载图片资源,并将图片资源数据以Uint8List返回,这个过程由PaintingBinding单例调用引擎层_instantiateImageCodec()实现。
-
在下载过程中会通过StreamController获取进度,信息包含:cumulativeBytesLoaded(已加载大小)、expectedTotalBytes(预计总大小)。
-
经过引擎层图片数据处理返回ui.Codec编解码器给到MultiFrameImageStreamCompleter然后由ImageProvider获取到ImageStreamCompleter。
-
图片加载执行方法会在_ImageState的生命周期中执行,通过调用ImageProvider的resolve()方法获取到ImageStream。
-
ImageProvider方法resolve()过程中会通过ImageConfiguration配置生成Key,通过key在PaintingBinding单例的ImageCache中查找图片缓存。
-
若存在图片缓存对象的ImageCompleter直接获取,不存在则需要执行ImageProvider的load()方法先加载到图片资源。
图片渲染过程
-
图片渲染过程发生在Image组件的_ImageState中。在特定生命周期时会调用_resolverImage() 获取ImageStream更新ImageStream资源并设置ImageStreamListener监听。
-
在调用_resolverImage()同时调用了ImageProvider的resolve()方法执行图片加载过程。
-
图片资源加载过程会通过_handleImageFrame()回调方法返回,返回对象重要对象为ImageInfo包含了图片的宽高、缩放比、图片数据ByteData
-
最终图片渲染由RawImage实现,其内部由RenderImage通过paintImage()在画布上绘制图片。
内容补充
version-1.17.0更新ImageCache
Flutter最新更新了1.17.0其中ImageCache新增了_liveImages。_liveImages主要作用前置常用图片缓存,当ImageCache缓存池优先清除之前的缓存图片可以采用liveImages缓存队列中的图片数据,通过ImageStream的removeListener方法可以了解它的生命周期所以适用于当前页面,退出当前页面会从缓存中清除。(个人源码阅读理解,若有误请补充说明)
前版本源代码
- 原先缓存逻辑是图片缓存先放在_PendingImage中,当获取到图片数据后移除PendingImage缓存将缓存添加到_cacheImage中。
- 获取缓存优先级:PendingImage -> cacheImage -> load()
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
// 直接从_pendingImages缓存中获取(可能图片还在加载中)
ImageStreamCompleter result = _pendingImages[key]?.completer;
if (result != null)
return result;
// 若为空则从cache中获取
final _CachedImage image = _cache.remove(key);
if (image != null) {
_cache[key] = image;
return image.completer;
}
// 如果缓存都为空则调用Provider的loader方法加载图片数据
try {
result = loader();
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
/// ImageStreamCompleter回调函数
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);
if (maximumSizeBytes > 0 && imageSize > maximumSizeBytes) {
_maximumSizeBytes = imageSize + 1000;
}
_currentSizeBytes += imageSize;
/// 移除加载中队列的图片缓存
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
/// 将图片存入缓存并计算剩余缓存大小等
_cache[key] = image;
_checkCacheSize();
}
/// 将图片加载中队列进行图片获取操作
if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener);
result.addListener(streamListener);
}
return result;
}
新版本源代码
- 新版本主要是新增了_trackLiveImage()、_touch()两个方法。在原先 _pendingImages和_cache基础上新增_liveImages缓存队列。
- _touch()执行缓存大小判断,将图片缓存放入_cache缓存队列。
- _trackLiveImage()为liveImage设置图片sizeBytes并添加回调清除图片缓存接口。
- 获取缓存优先级:pendingImage -> cacheImage -> liveImage -> load()
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
..... 省略相同部分代码
final _CachedImage image = _cache.remove(key);
if (image != null) {
/// 缓存添加图片后再向liveImage添加
_trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
_cache[key] = image;
return image.completer;
}
/// 若缓存中不存在则从liveImage缓存队列查找
final _CachedImage liveImage = _liveImages[key];
if (liveImage != null) {
_touch(key, liveImage, timelineTask);
return liveImage.completer;
}
/// 如果liveImages缓存中也没有则调用loader()加载图片资源。
try {
result = loader();
_trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
} catch (error, stackTrace) {
if (onError != null) {
onError(error, stackTrace);
return null;
} else {
rethrow;
}
}
/// 若缓存被禁用了 使用该变量放置图片数据
_PendingImage untrackedPendingImage;
/// 和原先回调方式相同,ImageStreamCompleter回调函数设置,回调后将图片从_PendingImage缓存队列中移除。
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);
_trackLiveImage(
key,
_LiveImage(
result,
imageSize,
() => _liveImages.remove(key),
),
debugPutOk: syncCall,
);
final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
if (untrackedPendingImage == null) {
_touch(key, image, listenerTask);
}
}
final ImageStreamListener streamListener = ImageStreamListener(listener);
/// 修改了原有增加缓存的逻辑 通过untrackedPendingImage不缓存图片。
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = _PendingImage(result, streamListener);
} else {
untrackedPendingImage = _PendingImage(result, streamListener);
}
result.addListener(streamListener);
}
/// liveImage预先加载
void _trackLiveImage(Object key, _LiveImage image, { bool debugPutOk = true }) {
_liveImages.putIfAbsent(key, () {
image.completer.addOnLastListenerRemovedCallback(image.handleRemove);
return image;
}).sizeBytes ??= image.sizeBytes;
}
/// liveImage加载后并添加到_cache中
void _touch(Object key, _CachedImage image, TimelineTask timelineTask) {
if (image.sizeBytes != null && image.sizeBytes <= maximumSizeBytes) {
_currentSizeBytes += image.sizeBytes;
_cache[key] = image;
_checkCacheSize(timelineTask);
}
}
总结
最后通过一张图总结Image加载过程,主要通过ImageProvider获取到图片资源将ImageStream给到ImageState。ImageState可通过ImageStream监听图片的加载进度,最后获取ImageInfo后通过RawImage绘制图片。