Flutter系列之Image加载原理
一、前言
最近在做的项目中,总是用到Image组件,所以就了解了一下Image的源码,顺便记录下来,和大家分享一下。
本文是基于1.12.13+hotfix.8的源码,以加载网路图片为例进行解读。毕竟自己还是个小白,如果有解读不对的地方,欢迎指正。
二、Image
Image继承了StatefulWidget,是用于显示图片的 Widget,最后通过内部的 RenderImage 绘制。
先看看Image结构,以Image.network为例:
文章图片
image.png 先简单介绍一下这些类,后续我们会一一详细介绍。
- Image用来显示图片。
- _ImageState处理生命周期,生成Widget。
- ImageProvider用来加载图片,生成key。
- NetWorkImage是具体执行下载的,将下载的图片转化成ui.Codec,然后由ImageStreamCompleter去处理。
- ImageStreamCompleter用来逐帧解析图片。
- ImageStream是存储加载结果监听器List的。
- MultiFrameImageStreamCompleter是多帧图片解析器。
- ImageStreamListener 实际监听加载结果
构造函数
Image.network(
String src,{
Key key,
@required this.image,
this.frameBuilder,
this.loadingBuilder,
...
this.filterQuality = FilterQuality.low,
}): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
// ...
super(key: key);
Image.network以命名构造函数创建Image对象时,会同时初始化实例变量image。
- src:图片的url
- image:必选参数,一个ImageProvide对象,图片的提供者,在调用的时候已经实例化,稍后会具体介绍。
//ImageProvider初始化
class ResizeImage extends ImageProvider<_SizeAwareCacheKey> {
...
static ImageProvider resizeIfNeeded(int cacheWidth, int cacheHeight, ImageProvider provider) {
if (cacheWidth != null || cacheHeight != null) {
return ResizeImage(provider, width: cacheWidth, height: cacheHeight);
}
return provider;
}
}
State 作为一个StatefulWidget,最重要的当然是State了。
@override
_ImageState createState() => _ImageState();
Image的主要构成就是两部分,和
接下来我们分别介绍一下这两部分。
三、_ImageState
Image是一个StatefulWidget,状态由_ImageState控制。_ImageState继承自State,其生命周期方法包括initState()、didChangeDependencies()、build()、dispose()、didUpdateWidget()等。我们先来看看_ImageState中都做了些什么。
成员变量
class _ImageState extends State with WidgetsBindingObserver {
ImageStream _imageStream;
ImageInfo _imageInfo;
bool _isListeningToStream = false;
···
}
- _imageStream
处理Image Resource的,ImageStream里存储着图片加载完毕的监听回调 - _imageInfo
Image的数据源信息:width和height以及ui.Image。 将ImageInfo里的ui.Image设置给RawImage就可以展示了。RawImage就是我们真正渲染的对象
- initState
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
//监听生命周期
}
- didChangeDependencies
@override
void didChangeDependencies() {
...
_resolveImage();
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream();
super.didChangeDependencies();
}
_resolveImage()方法是核心,我们来分析一下。
void _resolveImage() {
final ImageStream newStream =
widget.image.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null,
));
assert(newStream != null);
_updateSourceStream(newStream);
} void _updateSourceStream(ImageStream newStream) {
if (_imageStream?.key == newStream?.key)
return;
if (_isListeningToStream)
_imageStream.removeListener(_getListener());
if (!widget.gaplessPlayback)
setState(() { _imageInfo = null;
});
setState(() {
_loadingProgress = null;
_frameNumber = null;
_wasSynchronouslyLoaded = false;
});
_imageStream = newStream;
if (_isListeningToStream)
_imageStream.addListener(_getListener());
} ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) {
loadingBuilder ??= widget.loadingBuilder;
return ImageStreamListener(
_handleImageFrame,
onChunk: loadingBuilder == null ? null : _handleImageChunk,
);
}
1、 通过ImageProvider得到ImageStream 对象
2、 然后 _ImageState 利用 ImageStream 添加监听,等待图片数据
- didUpdateWidget
@override
void didUpdateWidget(Image oldWidget) {
super.didUpdateWidget(oldWidget);
if (_isListeningToStream &&
(widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) {
_imageStream.removeListener(_getListener(oldWidget.loadingBuilder));
_imageStream.addListener(_getListener());
}
if (widget.image != oldWidget.image)
_resolveImage();
}
- build
@override
Widget build(BuildContext context) {
Widget result = RawImage(
image: _imageInfo?.image,
...
);
if (!widget.excludeFromSemantics) {
result = Semantics(
...
);
}
...
return result;
}
四、ImageProvider
ImageProvider是一个抽象类,提供图片数据获取和加载的的接口,NetworkImage 、AssetImage 等均实现了这个接口。
它主要有两个功能:
- 提供图片数据源
- 缓存图片
abstract class ImageProvider {
//接收ImageConfiguration参数,返回ImageStream-图片数据流
ImageStream resolve(ImageConfiguration configuration) {
...
}
//清除指定key对应的图片缓存
Future evict({ ImageCache cache,ImageConfiguration configuration = ImageConfiguration.empty }) async {
...
}
//需要ImageProvider子类实现,不同的ImageProvider对key的定义逻辑不同
Future obtainKey(ImageConfiguration configuration);
// 需ImageProvider子类实现,加载图片数据
@protected
ImageStreamCompleter load(T key);
}
- resolve
获取数据流 - evict
清除缓存 - obtainKey
配合实现图片缓存 - load
加载图片数据源
#ImageProvider
ImageStream resolve(ImageConfiguration configuration) {
//1、创建图片数据流
final ImageStream stream = ImageStream();
T obtainedKey;
//
//2、错误处理
Future handleError(dynamic exception, StackTrace stack) async {
...
stream.setCompleter(imageCompleter);
imageCompleter.setError(...);
}
//3、创建一个新Zone,用来处理发生的错误,不干扰MainZone
final Zone dangerZone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) {
handleError(error, stackTrace);
}
)
);
dangerZone.runGuarded(() {
// 4、判断是否有缓存的相关逻辑
Future key;
try {
// 5、生成key,后续会用此key判断是否有缓存
key = obtainKey(configuration);
} catch (error, stackTrace) {
handleError(error, stackTrace);
return;
}
key.then((T key) {
// 6、缓存处理逻辑
obtainedKey = key;
final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
key,
() => load(key, PaintingBinding.instance.instantiateImageCodec),
onError: handleError,
);
if (completer != null) {
//7、stream设置ImageStreamCompleter对象
stream.setCompleter(completer);
}
}).catchError(handleError);
});
return stream;
}
这段代码中,我们需要重点看四个点,
- ImageStream
- ImageCache
- obtainKey 方法
- ImageStreamCompleter
ImageCache 在resolve 方法中调用了PaintingBinding.instance.imageCache.putIfAbsent方法(注释6处),这里的PaintingBinding.instance.imageCache 是 ImageCache的一个实例。PaintingBinding.instance和imageCache是单例的,所以说图片缓存是项目全局的。
const int _kDefaultSize = 1000;
// 最大缓存数量,默认1000
const int _kDefaultSizeBytes = 100 << 20;
// 最大缓存容量,默认100 MB
class ImageCache {
// 正在加载中的图片队列
final Map
putIfAbsent方法主要是先通过 key 判断内存中正在缓存的对象或者是否有缓存,如果有就返回该对象的ImageStreamCompleter ,否则就调用 loader 去加载并返回ImageStreamCompleter。
这里提醒大家两个地方:
- 图片缓存是在内存中,没有进行本地存储。
- 应用生命周期内,如果缓存没有超过上限,相同的图片(key相同)只会被下载一次。
#ImageStream
void setCompleter(ImageStreamCompleter value) {
assert(_completer == null);
_completer = value;
if (_listeners != null) {
final List initialListeners = _listeners;
_listeners = null;
initialListeners.forEach(_completer.addListener);
}
}
ImageStreamCompleter是一个抽象类,定义了管理图片加载过程的一些接口,Image Widget中正是通过它来监听图片加载状态的。每一个ImageStream对象只能设置一次,ImageStreamCompleter是为了辅助ImageStream解析和管理Image图片帧的,并且判断是否有初始化监听器,可以做一些初始化回调工作。
abstract class ImageStreamCompleter extends Diagnosticable {
final List<_ImageListenerPair> _listeners = <_ImageListenerPair>[];
ImageInfo _currentImage;
FlutterErrorDetails _currentError;
void addListener(ImageListener listener, { ImageErrorListener onError }) {...}
void removeListener(ImageListener listener) {... }
void reportError(...) {... }
@protected
void setImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
// Make a copy to allow for concurrent modification.
final List localListeners = List.from(_listeners);
for (ImageStreamListener listener in localListeners) {
try {
listener.onImage(image, false);
} catch (exception, stack) {
reportError(
...
);
}}}}
4.2 obtainKey key是图片缓存的一个唯一标识,也是判断该图片是否应该被缓存的唯一条件。这个key就是ImageProvider.obtainKey()方法的返回值,不同类型的ImageProvider对key的定义逻辑会不同,所以此方法需要ImageProvider子类去重写。我们以NetworkImage为例,看一下它的obtainKey()实现:
#NetworkImage
@override
Future obtainKey(image_provider.ImageConfiguration configuration) {
return SynchronousFuture(this);
}
其实就是创建一个future,然后将NetworkImage自身做为key返回。
那么又是如何判断key是否相等的呢?
#NetworkImage
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final NetworkImage typedOther = other;
return url == typedOther.url
&& scale == typedOther.scale;
}
在NetworkImage中,是将url+ scale(缩放比例)作为缓存中的key。只有url和scale相等,才算是有缓存。也就是说如果两张图片的url或scale只要有一个不同,便会重新下载并分别缓存。
4.3 load(T key)方法解析 load()是ImageProvider加载图片数据源的接口,不同ImageProvider的数据源加载方法不同,每个ImageProvider的子类必须实现它。比如NetworkImage类和AssetImage类,它们都是ImageProvider的子类,NetworkImage是从网络来加载图片数据,AssetImage则是从最终的应用包里来加载。
我们以NetworkImage为例,看看其load方法的实现:
#NetworkImage
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {final StreamController chunkEvents = StreamController();
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, chunkEvents), //调用_loadAsync
chunkEvents: chunkEvents.stream,
scale: key.scale,
...
);
}
MultiFrameImageStreamCompleter 是一个多帧图片管理器,是ImageStreamCompleter的一个子类。
MultiFrameImageStreamCompleter 需要一个Future
MultiFrameImageStreamCompleter({
@required Future codec,
@required double scale,
Stream chunkEvents,
InformationCollector informationCollector,
}) : assert(codec != null),
_informationCollector = informationCollector,
_scale = scale {
codec.then(_handleCodecReady, onError: (dynamic error, StackTrace stack) {
reportError(...);
});
if (chunkEvents != null) {
chunkEvents.listen(
(ImageChunkEvent event) {
if (hasListeners) {
// Make a copy to allow for concurrent modification.
final List localListeners = _listeners
.map((ImageStreamListener listener) => listener.onChunk)
.where((ImageChunkListener chunkListener) => chunkListener != null)
.toList();
for (ImageChunkListener listener in localListeners) {
listener(event);
}
}
}, onError: (dynamic error, StackTrace stack) {//...},
);
}
}
Codec类部分定义如下:
@pragma('vm:entry-point')
class Codec extends NativeFieldWrapperClass2 {
// 此类由flutter engine创建,不应该手动实例化此类或直接继承此类。
@pragma('vm:entry-point')
Codec._();
/// 图片中的帧数(动态图会有多帧)
int get frameCount native 'Codec_frameCount';
/// 动画重复的次数,0 -只执行一次,-1-循环执行
int get repetitionCount native 'Codec_repetitionCount';
/// 获取下一个动画帧
Future getNextFrame() {
return _futurize(_getNextFrame);
}
String _getNextFrame(_Callback callback) native 'Codec_getNextFrame';
}
我们可以看到Codec最终的结果是一个或多个(动图)帧,而这些帧最终会绘制到屏幕上。
MultiFrameImageStreamCompleter 的 codec参数值为_loadAsync方法的返回值,我们继续看_loadAsync方法的实现:
Future _loadAsync(
NetworkImage key,
StreamController chunkEvents,
) async {
try {
//下载图片
final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw Exception(...);
// 接收图片数据
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);
//PaintingBinding.instance.instantiateImageCodec(bytes)
} finally {
chunkEvents.close();
}
}
_loadAsync方法主要做了两件事:
- 下载图片。
- 对下载的图片数据进行解码。
在图片下载完成后调用了PaintingBinding.instance.instantiateImageCodec(bytes)对图片进行解码,instantiateImageCodec(...)也是一个Native API的包装,会调用Flutter engine的instantiateImageCodec方法,源码如下:
String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo, int targetWidth, int targetHeight)
native 'instantiateImageCodec';
codec的异步方法执行完成后会调用_handleCodecReady函数。
//MultiFrameImageStreamCompleter
void _handleCodecReady(ui.Codec codec) {
_codec = codec;
assert(_codec != null);
if (hasListeners) {
_decodeNextFrameAndSchedule();
}
}
该方法将codec对象保存起来,然后解码图片帧
#MultiFrameImageStreamCompleter
Future _decodeNextFrameAndSchedule() async {
try {
_nextFrame = await _codec.getNextFrame();
} catch (exception, stack) {
reportError(...);
return;
}
if (_codec.frameCount == 1) {
// This is not an animated image, just return it and don't schedule more frames.
_emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
return;
}
_scheduleAppFrame();
}
如果只有一帧,则执行_emitFrame函数。从帧数据中拿到图片帧对象根据缩放比例创建ImageInfo对象,然后设置显示的图片信息
#MultiFrameImageStreamCompleter
void _emitFrame(ImageInfo imageInfo) {
setImage(imageInfo);
_framesEmitted += 1;
}#ImageStreamCompleter
@protected
void setImage(ImageInfo image) {
_currentImage = image;
if (_listeners.isEmpty)
return;
// Make a copy to allow for concurrent modification.
final List localListeners =
List.from(_listeners);
for (ImageStreamListener listener in localListeners) {
try {
listener.onImage(image, false);
} catch (exception, stack) {
reportError(...);
}
}
}
五、Image加载流程总结 整个流程大概如下:
- Image构造函数先实例化一个ImageProvider
- 在_ImageState的didChangeDependencies方法中通过ImageProvider的resolve方法创建ImageStream对象,并关联一个ImageStreamCompleter,之后添加用于监听加载流程的ImageStreamListener1。
- 在获取ImageStreamCompleter的过程中,如果有缓存,就从缓存中获取ImageStreamCompleter,如果没有缓存,就调用ImageProvider的load方法去加载图片并返回ImageStreamCompleter对象,然后给ImageStreamCompleter添加ImageStreamListener2。
- load方法执行中会通过 http 下载图片,再经过PaintingBinding 编码转化后,得到ui.Codec可绘制对象,然后MultiFrameImageStreamCompleter调用_handleCodecReady方法把ui.Codec封装成ImageInfo。
- 接着MultiFrameImageStreamCompleter会调用setImage方法,此方法触发加载监听ImageStreamListener1和ImageStreamListener2。
- ImageStreamListener1回调到_ImageState,将ImageInfo保存, ImageStreamListener2的回调会把Image缓存下来。
- _ImageState的 build方法中的会根据ImageInfo构建一个 RawImage 对象。
- 最后 RawImage中的 RenderImage 通过paint方法绘制Widget。
//修改缓存最大值
const int _kDefaultSize = 100;
const int _kDefaultSizeBytes = 50 << 20;
//退出页面清除缓存
@override
void dispose() {
PaintingBinding.instance.imageCache.clear();
super.dispose();
}
七、添加磁盘缓存 【Flutter系列之Image加载原理】上面我们已经知道,Image只有内存缓存,没有本地缓存。那么我们如何添加本地缓存呢?其实只需要改进NetWorkImage的_loadAsync方法。
Future _loadAsync(NetworkImage key,StreamController chunkEvents, image_provider.DecoderCallback decode,) async {
try {
assert(key == this);
//--------新增代码1 begin--------------
// 判断是否有本地缓存
final Uint8List cacheImageBytes = await ImageCacheUtil.getImageBytes(key.url);
if(cacheImageBytes != null) {
return decode(cacheImageBytes);
}
//--------新增代码1 end--------------//...省略
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
//--------新增代码2 begin--------------
// 缓存图片数据到本地,需要定制具体的缓存策略
await ImageCacheUtil.saveImageBytesToLocal(key.url, bytes);
//--------新增代码2 end--------------return decode(bytes);
} finally {
chunkEvents.close();
}
}
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- python学习之|python学习之 实现QQ自动发送消息