flutter源码学习笔记-图片加载流程
- 2020 年 4 月 5 日
- 笔记
本文基于1.12.13+hotfix.8版本源码分析。
0、大纲
- Image
- ImageProvider
- 图片数据加载 ImageStream、ImageStreamCompleter
- 缓存池 PaintingBinding#imageCache
- 网络图片加载
1、Image
点击进入源码,可以看到Image继承自StatefulWidget,那么重点自然在State里面。跟着生命周期走,可以发现在didUpdateWidget中调用了这个方法:
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 (_isListeningToStream) _imageStream.addListener(_getListener()); } ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) { loadingBuilder ??= widget.loadingBuilder; return ImageStreamListener( _handleImageFrame, onChunk: loadingBuilder == null ? null : _handleImageChunk, ); }
在这里调用了image(ImageProvider)的resolve方法获取到一个ImageStream,并给这个流设置了监听器。从名字上,不难猜出这是个图片数据流,在listener拿到数据后会调用setState(() {})方法进行rebuild,这里不再贴代码。
2、ImageProvider
在上面我们看到了Image是需要接收图片数据进行绘制的,那么,这个数据是在哪里解码的?又是哪里发送过来的?
带着疑问,我们先进到ImageProvider的源码,可以发现其实这个类非常简单,代码量也不多,可以看看resolve方法的核心部分:
Future<T> key; try { key = obtainKey(configuration); } catch (error, stackTrace) { handleError(error, stackTrace); return; } 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);
可以看到,这里会异步获取到一个key,然后从管理在PaintingBinding中的缓存池查找图片流。继续看关键的obtainKey和load方法,去到定义的地方,可以发现这两个都是子类实现的。从注释中可以看到,obtainKey的功能就是根据传入的ImageConfiguration生成一个独一无二的key(废话),而load方法则是将key转换成为一个ImageStreamCompleter对象并开始加载图片。
那么,我们从最简单的MemoryImage入手,首先看看obtainKey:
@override Future<MemoryImage> obtainKey(ImageConfiguration configuration) { return SynchronousFuture<MemoryImage>(this); }
可以看到,就只是把自己包了一层再返回,并没有什么特殊。接着看load:
@override ImageStreamCompleter load(MemoryImage key, DecoderCallback decode) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), scale: key.scale, ); } Future<ui.Codec> _loadAsync(MemoryImage key, DecoderCallback decode) { assert(key == this); return decode(bytes); }
同样非常简单,就是创建了一个ImageStreamCompleter的子类对象,同时传入了一个包装了解码器的Future(这个解码器是PaintingBinding.instance.instantiateImageCodec,内部调用native方法进行图片解码)。
看到这里,相信基本有猜想了,数据和解码器都提供了,看来ImageStreamCompleter就是我们要看的数据源提供者。
3、图片数据加载ImageStream、ImageStreamCompleter
废话不多说,直接看MultiFrameImageStreamCompleter,可以看到直接在构造函数中获取codec对象,在获取到以后就会去获取解码数据,下面是简化的代码片段:
// 构造函数中获取codec codec.then<void>(_handleCodecReady, onError: (dynamic error, StackTrace stack) {// 略}); void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); if (hasListeners) { // 拿到codec之后解码数据 _decodeNextFrameAndSchedule(); } } Future<void> _decodeNextFrameAndSchedule() async { try { _nextFrame = await _codec.getNextFrame(); } catch (exception, stack) { // 略 return; } if (_codec.frameCount == 1) { // 发送数据 _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale)); return; } _scheduleAppFrame(); }
看到这里,终于找到了发送数据的地方,_emitFrame里面会调用setImage,而后在setImage中会找到listener并将数据发送,而listener就是widgets.Image注册的监听器。
4、缓存池PaintingBinding#imageCache
看完了加载流程,我们看看缓存池的缓存逻辑,回到ImageProvider的resolve方法,这里有个关键点,传给PaintingBinding的是个创建方法,而非实体。进入其源码可以看到是先检测cache中是否存在该对象,存在则直接返回,不存在才会调用load方法进行创建:
final _CachedImage image = _cache.remove(key); if (image != null) { // 有缓存就直接返回 _cache[key] = image; return image.completer; } try { // 没找到缓存就调外面传入的loader()进行创建 result = loader(); } // catch部分省略
并且,在刚创建时缓存中的对象是个PendingImage,这东西可以理解为类似一个占位符的作用,当图片数据加载完毕后才替换成实际数据对象CacheImage:
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(); } // 这里创建了一个PendingImage插入缓存 if (maximumSize > 0 && maximumSizeBytes > 0) { final ImageStreamListener streamListener = ImageStreamListener(listener); _pendingImages[key] = _PendingImage(result, streamListener); // 监听加载状态,result就是ImageStreamCompleter result.addListener(streamListener); }
5、网络图片加载
看完最基本的图片数据加载,接下来看看NetworkImage如何加载网络图片。看核心的load方法:
ImageStreamCompleter load(image_provider.NetworkImage key, image_provider.DecoderCallback decode) { final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); return MultiFrameImageStreamCompleter( // 关键点1,加载、解析数据 codec: _loadAsync(key, chunkEvents, decode), // 关键点2,分块下载事件流传给completer用 chunkEvents: chunkEvents.stream, scale: key.scale, ); }
直接进入关键方法,看NetworkImage的_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); headers?.forEach((String name, String value) { request.headers.add(name, value); }); final HttpClientResponse response = await request.close(); if (response.statusCode != HttpStatus.ok) // 可以看到,图片下载失败是会抛异常的 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); } finally { chunkEvents.close(); } }
这里有2个点:
(1)通过HttpClient进行图片下载,下载失败或者数据为空都会抛异常,这里要做好异常处理。另外,从上面的图片缓存逻辑可以看到,flutter默认是只有内存缓存的,磁盘缓存需要自己处理,可以在这里入手处理;
(2)通过consolidateHttpClientResponseBytes接收数据,并将下载进度转成ImageChunkEvent发送出去。可以看看MultiFrameImageStreamCompleter对ImageChunkEvent的处理:
if (chunkEvents != null) { chunkEvents.listen( (ImageChunkEvent event) { if (hasListeners) { // 把这个事件传递给ImageStreamListener的onChunk方法 final List<ImageChunkListener> localListeners = _listeners .map<ImageChunkListener>((ImageStreamListener listener) => listener.onChunk) .where((ImageChunkListener chunkListener) => chunkListener != null) .toList(); for (ImageChunkListener listener in localListeners) { listener(event); } } } ); }
顺着_listeners的来源,一路往上找,最后可以看到onChunk方法是这里传进来的:
ImageStreamListener _getListener([ImageLoadingBuilder loadingBuilder]) { loadingBuilder ??= widget.loadingBuilder; return ImageStreamListener( _handleImageFrame, onChunk: loadingBuilder == null ? null : _handleImageChunk, ); }
widget.loadingBuilder即自定义loading状态的方法。