导语
在使用flutter 自带图片组件的过程中,大家有没有考虑过flutter是如何加载一张网络图片的? 以及对自带的图片组件我们可以做些什么优化?
问题
flutter 网络图片是怎么请求的?
图片请求成功后是这么展示的? gif的每一帧是怎么支持展示的?
如何支持图片的磁盘缓存?
接下来,让我们带着问题一起探究flutter 图片组件的内部原理
本文源码分析以flutter-1.22版本为准,只涉及到dart端,c层图片解码不涉及
Image的核心类图及其关系
自己重新画一张
- Image,是一个statefulWidget,flutter image的核心入口类,包含了network,file,assert,memory这几个主要的功能,分包对应网络图片,文件图片,APP内置assert图片,从文件流解析图片
- _ImageState,由于Image是statefulWidget,所以核心代码都在_ImageState
- ImageStream ,处理图片资源,ImageState和ImageStreamCompleter的桥梁
- ImageInfo ,图片原生信息存储者
- ImageStreamCompleter,可以理解为一帧帧解析图片,并把解析的数据回调给展示方,主要有两个实现类
-
- OneFrameImageStreamCompleter单帧图片解析器(貌似没在用)
-
- MultiFrameImageStreamCompleter多帧图片解析器,源码里所有图片都是默认使用这个了
- ImageProvider,图片加载器,不同的加载方式有不同的实现
-
- NetworkImage 网络加载图片
-
- MemoryImage 从二进制流加载图片
-
- AssetImage 加载asset里的image
-
- FileImage 从文件中加载图片
- ImageCache ,flutter自带的图片缓存,只有内存缓存,官方自带cache ,最大个数100,最大内存100MB
- ScrollAwareImageProvider,避免图片在快速滑动中加载
网络图片的加载过程
// 网络图片 Image.network(imgUrl, //图片链接 width: w, height: h), )
上文中提到过,Image是个StatefulWidget,那核心逻辑看对应的ImageState,ImageState继承自State,State的生命周期我们知道,首次初始化时按InitState()->didChangeDependencies->didUpdateWidget()-> build()顺序执行
ImageState的InitState没做什么,图片请求的发起是在didChangeDependencies里做的
// ImageState->didChangeDependencies @override void didChangeDependencies() { // ios在辅助模式下的配置,不影响主流程,我们不分析 _updateInvertColors(); // 核心方法,开始请求解析图片,从这里开始,provier,stream,completer开始悉数登场 _resolveImage(); // 这个判断可以认为是,当前widget 在tree中是否还是激活状态 if (TickerMode.of(context)) _listenToStream(); else _stopListeningToStream(); super.didChangeDependencies(); }
再看ImageState里的_resolveImage方法
void _resolveImage() { // ScrollAwareImageProvider代理模式,它本身也是继承的ImageProvider, // 它的功能是防止在快速滚动时加载图片 final ScrollAwareImageProvider provider = ScrollAwareImageProvider( context: _scrollAwareContext, imageProvider: widget.image, ); // 这里调用了ImageProvider的resolve方法,图片请求的主流程 final ImageStream newStream = provider.resolve(createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width, widget.height) : null, )); assert(newStream != null); // 对resolve返回的Stream注册监听,这个监听很重要,决定了后续的图片展示(包括gif) // 刷新当前图片展示一次,例如帧数,加载状态等等 _updateSourceStream(newStream); }
我们接着看ImageProvider的resolve方法
// 这方法初次看比较绕,其实就干了三个事 // 1. 创建了一个ImageStream // 2. 创建一个Key,key由具体的provider自己实现,这个key用在后面ImageCache里 // 3. 把接下来的流程封装在一个Zone里,捕获了同步异常和异步异常,不了解Zone的同学可以参考我另一篇文章 @nonVirtual ImageStream resolve(ImageConfiguration configuration) { assert(configuration != null); final ImageStream stream = createStream(configuration); // 创建了key,把后续的流程封装在zone里,源码我不贴了,感兴趣的同学自己看下 _createErrorHandlerAndKey( configuration, (T key, ImageErrorListener errorHandler) { resolveStreamForKey(configuration, stream, key, errorHandler); }, (T? key, dynamic exception, StackTrace? stack) async { await null; // wait an event turn in case a listener has been added to the image stream. final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter(); stream.setCompleter(imageCompleter); InformationCollector? collector; assert(() { collector = () sync* { yield DiagnosticsProperty('Image provider', this); yield DiagnosticsProperty ('Image configuration', configuration); yield DiagnosticsProperty ('Image key', key, defaultValue: null); }; return true; }()); imageCompleter.setError( exception: exception, stack: stack, context: ErrorDescription('while resolving an image'), silent: true, // could be a network error or whatnot informationCollector: collector, ); }, ); return stream; }
接着看resolveStreamForKey方法,在1.22里,默认的provider都是ScrollAwareImageProvider,ScrollAwareImageProvider重写了resolveStreamForKey,这里有滚动控制加载的逻辑,但最终调用的还是ImageProvier的resolveStreamForKey
// ImageProvier -> resolveStreamForKey @protected void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) { // streem中已经有completer了,从缓存中拿, if (stream.completer != null) { final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => stream.completer!, onError: handleError, ); assert(identical(completer, stream.completer)); return; } // 如果是首次,新建一个completer,然后会执行load这个函数,就是putIfAbsent的第二个入参 final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, ); // 赋值,注意这里,后面讲图片展示的时候会说到这里 if (completer != null) { stream.setCompleter(completer); } }
接着看ImageProvider的load,load方法就是图片的具体加载方法,不同的provider有不同的实现,此时我们关注NetworkImage的Provier里的实现
// NetworkImage @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 StreamControllerchunkEvents = StreamController (); // MultiFrameImageStreamCompleter是多帧解析器,默认使用的是就是这个,所以默认支持gif return MultiFrameImageStreamCompleter( codec: _loadAsync(key as NetworkImage, chunkEvents, decode), // 异步加载图片,我们接着看这个方法 chunkEvents: chunkEvents.stream, //加载过程的回调 scale: key.scale, debugLabel: key.url, informationCollector: () { return [ DiagnosticsProperty ('Image provider', this), DiagnosticsProperty ('Image key', key), ]; }, ); }
接着看NetworkImage的_loadAsync
// 这里就很清晰了吧,内置的HttpClient去加载 Future_loadAsync( NetworkImage key, StreamController 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); } catch (e) { // Depending on where the exception was thrown, the image cache may not // have had a chance to track the key in the cache at all. // Schedule a microtask to give the cache a chance to add the key. scheduleMicrotask(() { PaintingBinding.instance!.imageCache!.evict(key); }); rethrow; } finally { chunkEvents.close(); } }
至此,第一个问题回答完毕,那当图片数据请求成功后,是怎么回调到ImageState并展示到界面中的呢?
网络图片数据的回调和展示过程
要看回调和展示,我们从终点ImageState的build方法开始看
// 很容易发现RawImage,RawImage是实际渲染图片的widget,这么说其实也不对,RenderImage才是最终渲染的 // 可以看到RawImage的第一个参数_imageInfo?.image,那_imageInfo?.image是什么时候赋值的? Widget result = RawImage( image: _imageInfo?.image, debugImageLabel: _imageInfo?.debugLabel, width: widget.width, height: widget.height, scale: _imageInfo?.scale ?? 1.0, color: widget.color, 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, );
还记得第一部分提到的_updateSourceStream(newStream);方法吗?在这个方法里对ImageStrem设置了一个监听
// 设置了监听 _imageStream.addListener(_getListener()); // ImageStreamListener ImageStreamListener _getListener({bool recreateListener = false}) { if(_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, // 每一帧图片解析完,代表可以展示这一帧了 onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, // 图片加载互调 onError: widget.errorBuilder != null // 图片加载错误互调 ? (dynamic error, StackTrace stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); } : null, ); } return _imageStreamListener; }
接着看ImageState的_handleImageFrame
// 很简单,就是setState,可以看到这里赋值了_imageInfo void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _imageInfo = imageInfo; _loadingProgress = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber + 1; _wasSynchronouslyLoaded |= synchronousCall; }); }
那么这个_imageStreamListener 是什么时候回调的呢? 还记得第一步加载过程最后一步的MultiFrameImageStreamCompleter吗?
// MultiFrameImageStreamCompleter就是支持gif的多帧解析器,还有一个OneFrameImageStreamCompleter,但已经不用了 MultiFrameImageStreamCompleter({ required Futurecodec, required double scale, String? debugLabel, Stream ? chunkEvents, InformationCollector? informationCollector, }) : assert(codec != null), _informationCollector = informationCollector, _scale = scale { this.debugLabel = debugLabel; // _handleCodecReady就是图片加载完的回调,我们看看他内部干了什么 codec.then (_handleCodecReady, onError: (dynamic error, StackTrace stack) { // 捕获错误并上报 }); // 监听回调 if (chunkEvents != null) { chunkEvents.listen(reportImageChunkEvent, onError: (dynamic error, StackTrace stack) { reportError( context: ErrorDescription('loading an image'), exception: error, stack: stack, informationCollector: informationCollector, silent: true, ); }, ); } }
这里回答了第二个问题,gif的每帧是怎么支持的,关键就是MultiFrameImageStreamCompleter这个类, 接着看MultiFrameImageStreamCompleter的_handleCodecReady
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); if (hasListeners) { // 看函数名就知道了,解析下一帧并执行 _decodeNextFrameAndSchedule(); } }
MultiFrameImageStreamCompleter的_decodeNextFrameAndSchedule()
Future_decodeNextFrameAndSchedule() async { try { // 获得下一帧,这一步在C中处理 _nextFrame = await _codec!.getNextFrame(); } catch (exception, stack) { reportError( context: ErrorDescription('resolving an image frame'), exception: exception, stack: stack, informationCollector: _informationCollector, silent: true, ); return; } // 帧数不等于1,说明图片有多帧 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, debugLabel: debugLabel)); return; } // 如果只有一帧,_scheduleAppFrame最终也会走到_emitFrame _scheduleAppFrame(); }
接着看MultiFrameImageStreamCompleter的_emitFrame
// 调用了setImage void _emitFrame(ImageInfo imageInfo) { setImage(imageInfo); _framesEmitted += 1; }
ImageStreamCompleter的setImage
@protected void setImage(ImageInfo image) { _currentImage = image; if (_listeners.isEmpty) return; // Make a copy to allow for concurrent modification. final ListlocalListeners = List .from(_listeners); for (final ImageStreamListener listener in localListeners) { try { // 在这里回调了onImage,那这个回调是哪里注册的呢? 回到ImageStream的addLister里 listener.onImage(image, false); } catch (exception, stack) { } } }
ImageStream的addLister里
void addListener(ImageStreamListener listener) { // 这里破案了,_completer 不为null的时候,注册了回调,而ImageStream的completer在ImageStream被创建的还是就赋值了 // 所以前面的listener.onImage(image, false);最终会回调到ImageState里的_imageStreamListener if (_completer != null) return _completer!.addListener(listener); _listeners ??=[]; _listeners!.add(listener); }
至此,图片的是展示流程也分析完毕,第二个问题也回答完了。
补上图片内存缓存的源码分析
首先要说明的是,flutter内存缓存默认只有内存缓存,也就意味着如果杀进程重启,图片就需要重新加载了。
1.22的内存缓存主要分三部分,相比1.17增加了一部分
_pendingImages 正在加载中的缓存,这个有什么作用呢? 假设Widget1加载了图片A,Widget2也在这个时候加载了图片A,那这时候Widget就复用了这个加载中的缓存
_cache 已经加载成功的图片缓存,这个很好理解
_liveImages 存活的图片缓存,看代码主要是在CacheImage之外再加一层缓存,在CacheImage被清楚后,
对于一张图片,当首次加载时,首先会在_pendingImages中,注意此时图片还未加载成功,所以如果有复用的情况,会命中_pendingImages,当图片请求成功后,在_cache和_liveImages都会保存一份,此时_pendingImages会移除。 当超过缓存中的最大数时,会从_cache里按照LRU的规则删除
如何支持图片的磁盘缓存
在看完整个流程后,对磁盘缓存应该也有思路了。第一个是可以自定义ImageProvider,在图片数据请求成功后写入磁盘缓存,不过对于混合项目来说,更好的方式应该是替换图片的网络请求方式,利用channel和原生(Android ,ios)的图片库加载图片,这样可以复用原生图片库的磁盘缓存,但也有缺陷,在效率上会有降低,毕竟多了内存的多次拷贝和channel通信。
总结
本文只是分析了Image.Network的加载和展示过程,而且也只是涉及到了dart端代码。总的来说,整个流程并不复杂,其他诸如Image.Memory,Image.File 原理都是一样的,区别只是各自的ImageProvider不一样,我们也可以自定义ImageProvider实现自己想要的效果。
以上就是flutter图片组件核心类源码解析的详细内容,更多关于flutter图片组件核心类的资料请关注脚本之家其它相关文章!