目录
- 一、从构造函数开始
- 二、图片下载入口
- 2.1、ScrollAwareImageProvider
- 2.2、ImageConfiguration
- 2.3、ImageStream
- 三、图片流和Key
- 四、根据key来处理图片流
- 4.1、ImageCache
- 4.2、 load
- 五、图片下载
- 六、添加观察者实现界面更新
- 总结
Flutter原生支持在Image
组件上显示网络图片,最简单的使用方式如下,调用Image
的命名构造方法Image.network
即可实现网络图片的下载显示。
Widget image = Image.network(imageUrl);
那么,它内部是如何实现的呢?是否有做缓存处理或其他优化操作呢?带着疑问,我们一起来看下它的底层究竟是如何实现的。
一、从构造函数开始
我们以最简单的调用方式举例,当我们使用Image.network(imageUrl)
这种方式来显示图片时,Image
组件内部image
属性就会被赋值NetworkImage
。
// 此为简化过的Image组件类结构 class Image extends StatefulWidget { Image.network( String src, ) : image = NetworkImage(src); // 图片数据处理的基类 final ImageProvider image; }
这里引出了一个类叫NetworkImage
,它是ImageProvider
的子类,专门实现网络图片的下载和解析逻辑。当然你直接点进去看到的其实是个抽象类,并不是真正实现下载逻辑的地方,真正实现网络图片下载解析的在'_network_image_io.dart’
这个文件下。构造函数知道这些就够了。接下来就看Image
是在何时触发网络图片的下载的。
二、图片下载入口
Image
是一个StatefulWidget
,它又一个对应的State
叫_ImageState
。在这个_ImageState
的生命周期中,控制着图片的下载过程。
State的生命周期可以简单的分为:构造函数 → initState → didChangeDependencies → build
因此,我们顺着这个顺序找,很快看到一个可疑的地方,didChangeDependencies
中的_resolveImage
方法。而TickerMode
则是用于控制动画的,在这里被用于判断是否禁用了动画。关于TickerMode
的相关介绍,可以看下这篇文章
// 完整源码 @override void didChangeDependencies() { _updateInvertColors(); // 处理图片的入口 _resolveImage(); // 当动画被禁用时,图片也是无法显示的,这个 if (TickerMode.of(context)) // 添加图片流处理的监听 _listenToStream(); else _stopListeningToStream(keepStreamAlive: true); super.didChangeDependencies(); }
我们进入到_resolveImage
方法中去。
void _resolveImage() { // ScrollAwareImageProvider包装了我们的NetworkImage 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); }
_resolveImage
方法就做了三件事。
1、用ScrollAwareImageProvider
包装了NetworkImage
2、创建图片流对象ImageStream
3、更新图片流
2.1、ScrollAwareImageProvider
ScrollAwareImageProvider
也是ImageProvider
的子类,它的作用很简单,就是防止在快速滑动的时候加载图片,当存在快速滑动时,会将解析图片的工作放到下一帧处理。至于具体如何实现,我们放在后面再提。
2.2、ImageConfiguration
ImageConfiguration
由方法createLocalImageConfiguration
创建,保存了图片的基本配置信息,如Bundle
,屏幕项目比devicePixelRatio
,本地化local
,尺寸size
,平台platform
等。
2.3、ImageStream
表示一个图片流,可以添加观察者ImageStreamCompleter
来监听图片是否处理完成。一个图片流可以添加多个观察者。
ImageStream
由provider
的resolve
方法调用后创建。通过源码可知,此处的provider
就是ScrollAwareImageProvider
对象。但是它内部并没有实现resolve
方法,因此此处调用的是父类ImageProvider
的resolve
方法。
三、图片流和Key
以下代码截取自ImageProvider,并且删减了无关代码。
ImageStream resolve(ImageConfiguration configuration) { // 创建流,这里直接调用了ImageStream的构造函数,并没有用到configuration final ImageStream stream = createStream(configuration); // 关键在这里,这里会根据configuration创建一个唯一key _createErrorHandlerAndKey( configuration, // 成功的回调 (T key, ImageErrorListener errorHandler) { resolveStreamForKey(configuration, stream, key, errorHandler); }, // 下面是错误回调,可以不关注 (T? key, Object exception, StackTrace? stack) async { await null; // wait an event turn in case a listener has been added to the image stream. InformationCollector? collector; if (stream.completer == null) { stream.setCompleter(_ErrorImageCompleter()); } stream.completer!.reportError( exception: exception, stack: stack, context: ErrorDescription('while resolving an image'), silent: true, // could be a network error or whatnot informationCollector: collector, ); }, ); return stream; }
resolve
方法的作用是创建图片流对象ImageStream
,并根据传入的图片配置信息configuration
,创建对应的Key
,这个Key
用于图片缓存。
那么这个key
到底是怎么创建的呢,我们进入到_createErrorHandlerAndKey
方法中查看。关键代码如下,已删除无关代码。
Future<T> key; try { 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);
可以看到方法实现中调用了ImageProvider
的obtainKey
方法,而这个方法在ImageProvider
并没有具体实现,需要子类完成对应的实现。
Future<T> obtainKey(ImageConfiguration configuration);
还记得上文的分析不,我们说传入的imageProvider
实例是ScrollAwareImageProvider
对象,因此对应的实现也要到这个类中去查找。很快,我们找到obtainKey
方法的实现,可以看到它做了个透传,具体是由它包装的类也就是NetworkImage
来实现的。
@override Future<T> obtainKey(ImageConfiguration configuration) => imageProvider.obtainKey(configuration);
那么,我们就去NetworkImage
找obtainKey
。
注意下真正的NetworkImage
实现是在_network_image_io.dart
文件下的。
Future<NetworkImage> obtainKey(image_provider.ImageConfiguration configuration) { return SynchronousFuture<NetworkImage>(this); }
到这,我们就知道了NetworkImage
的key
为SynchronousFuture
。
获取到key后的下一步就是调用_createErrorHandlerAndKey
方法的successCallback
回调。从而触发了下一个流程resolveStreamForKey
。
_createErrorHandlerAndKey( configuration, (T key, ImageErrorListener errorHandler) { // 拿到Key之后的回调 resolveStreamForKey(configuration, stream, key, errorHandler); } )
四、根据key来处理图片流
还是回到子类ScrollAwareImageProvider
中,它重写了父类的resolveStreamForKey
方法,前文提到,ScrollAwareImageProvider
是用来防止列表在快速滑动的时候来加载图片的,那么它是如何实现的?我们就从resolveStreamForKey
这个方法中来一探究竟。
// 以下代码已去掉无关逻辑 @override void resolveStreamForKey( ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError, ) { // 滑动过快 if (Scrollable.recommendDeferredLoadingForContext(context.context!)) { SchedulerBinding.instance!.scheduleFrameCallback((_) { // 放入下一帧再尝试处理,如果下一帧还是过快,那么将一直被推迟 scheduleMicrotask(() => resolveStreamForKey(configuration, stream, key, handleError)); }); return; } // 当前可以加载,那么透传给包装的imageProvider来处理,这里是NetworkImage imageProvider.resolveStreamForKey(configuration, stream, key, handleError); }
Scrollable
用于滑动组件,它有个方法叫recommendDeferredLoadingForContext
,表示是否建议延迟加载。内部最终是根据滑动速度和当前设备的最大物理尺寸的边去比较,如果大于,表示速度过快,那么就建议延迟。具体逻辑在scroll_physics.dart
文件下。这里不多做介绍。
一旦当前应用处于滑动状态,并且速度过快,那么,图片的加载将会被推迟到下一帧再进行尝试。因此我们说,当处于快速滑动时,图片是无法加载的。
当判断可以加载图片时,操作流将会被移交给被包装类imageProvider
,这里是NetworkImage
来处理。但是,NetworkImage
没有实现resolveStreamForKey
方法,因此最终还是跑到了ImageProvider
类中的resolveStreamForKey
方法下。
@protected void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) { // 第一次进来还没有设置completer,因此不会进入这个分支中 if (stream.completer != null) { final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => stream.completer!, onError: handleError, ); assert(identical(completer, stream.completer)); return; } final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, ); if (completer != null) { stream.setCompleter(completer); } }
当第一次加载网络图的时候,会直接走到下面这个逻辑中。这里涉及到一个很重要的类,ImageCache
。它是做图片缓存用的。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, );
4.1、ImageCache
图片缓存类,只做了内存缓存。它由PaintingBinding持有,是一个单利。它的内部通过三个Map来缓存图片。
// 加载中的图片 final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{}; // 缓存中的图片 final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{}; // 正在使用的图片 final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{}
从图片缓存器中获取图片的逻辑集中在putIfAbsent
方法中。以下代码已经去掉无关代码。
ImageStreamCompleter? putIfAbsent(Object key, ImageStreamCompleter Function() loader, { ImageErrorListener? onError }) { TimelineTask? timelineTask; TimelineTask? listenerTask; ImageStreamCompleter? result = _pendingImages[key]?.completer; // 正在加载,直接返回 if (result != null) { return result; } // 这边有个小知识,dart中的Map是有顺序的,因此利用这点可以实现LRU算法。 // 最近用到了这图片,因此删除对应键值对,并更新,就能让它的位置处于前面 final _CachedImage? image = _cache.remove(key); if (image != null) { // 更新 _liveImages _trackLiveImage( key, image.completer, image.sizeBytes, ); _cache[key] = image; return image.completer; } final _LiveImage? liveImage = _liveImages[key]; if (liveImage != null) { // 更新 _cache,这里还会根据最大缓存数量和大小来最限制 _touch( key, _CachedImage( liveImage.completer, sizeBytes: liveImage.sizeBytes, ), timelineTask, ); return liveImage.completer; } // 加载流程,这是个回调,由各ImageProvider子类来实现 try { result = loader(); // 下载完更新 _liveImages _trackLiveImage(key, result, null); } catch (error, stackTrace) { if (onError != null) { onError(error, stackTrace); return null; } else { rethrow; } } bool listenedOnce = false; _PendingImage? untrackedPendingImage; // 设置图片加载监听,一旦加载完毕,那么会删除_pendingImages下对应的图片,并移除监听。同时更新_cache和_liveImages void listener(ImageInfo? info, bool syncCall) { int? sizeBytes; if (info != null) { sizeBytes = info.sizeBytes; 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; } // 设置加载监听,主要用来管理_pendingImages final ImageStreamListener streamListener = ImageStreamListener(listener); if (maximumSize > 0 && maximumSizeBytes > 0) { _pendingImages[key] = _PendingImage(result, streamListener); } else { untrackedPendingImage = _PendingImage(result, streamListener); } result.addListener(streamListener); return result; }
4.2、 load
一旦在ImageCache
中找不到缓存的图片,就会通过loader
回调出来,走真正的下载流程。
final ImageStreamCompleter? completer = PaintingBinding.instance!.imageCache!.putIfAbsent( key, // 本地图片找不到,需要去对应的ImageProvider子类里实现加载逻辑 () => load(key, PaintingBinding.instance!.instantiateImageCodec), onError: handleError, );
还是先看ScrollAwareImageProvider
类,里面实现了load
方法,并透传给了NetworkImage
来实现。
@override ImageStreamCompleter load(T key, DecoderCallback decode) => imageProvider.load(key, decode);
在NetworkImage
下,可以找到对应的load
方法实现。里面有个_loadAsync
方法,它就是我们要找的图片下载核心代码。
@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: () => <DiagnosticsNode>[ DiagnosticsProperty<image_provider.ImageProvider>('Image provider', this), DiagnosticsProperty<image_provider.NetworkImage>('Image key', key), ], ); }
五、图片下载
饶了一大圈,终于来到了下载图片的地方了。可以看到下载图片的逻辑很简单,创建一个下载的http请求,设置header,下载图片。一旦下载成功,就会通过decode这个回调将图片的二进制数据返回出去decode(bytes)
。
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) { // The network may be only temporarily unavailable, or the file will be // added on the server later. Avoid having future calls to resolve // fail to check the network again. await response.drain<List<int>>(<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) { // 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(); } }
而回调出去的这些二进制数据,是在MultiFrameImageStreamCompleter
中被处理的。MultiFrameImageStreamCompleter
是ImageStreamCompleter
的子类,用于管理多帧图片的加载。
在MultiFrameImageStreamCompleter
的构造方法中,我们可以看到它对codec
做了回调处理。而这个codec就是前面提到的_loadAsync
异步方法。
MultiFrameImageStreamCompleter({ required Future<ui.Codec> codec, required double scale, String? debugLabel, Stream<ImageChunkEvent>? chunkEvents, InformationCollector? informationCollector, }) : assert(codec != null), _informationCollector = informationCollector, _scale = scale { this.debugLabel = debugLabel; // 这里处理了_loadAsync的回调 codec.then<void>(_handleCodecRead);
_handleCodecRead
方法中回判断是否有观察者,有就进入解码流程。
void _handleCodecReady(ui.Codec codec) { _codec = codec; assert(_codec != null); if (hasListeners) { // 存在观察者,开始解码 _decodeNextFrameAndSchedule(); } }
_decodeNextFrameAndSchedule
方法可以看成是图片的解码方法,当然实际解码的地方位于更底层的Native。图片解码后会将信息保存在FrameInfo
中,由_nextFrame
持有。这里我们只考虑单帧图片,不考虑gif图。解码后的信息会被封装在ImageInfo
中,其中image
就是真正的图片数据。并调用_emitFrame
方法,更新图片信息。而_emitFrame
方法则主要是调用了setImage
来通知观察者更新。我们直接看setImage
方法即可。
Future<void> _decodeNextFrameAndSchedule() async { _nextFrame?.image.dispose(); _nextFrame = null; try { // 解码得到一帧图片信息,保存在FrameInfo中 _nextFrame = await _codec!.getNextFrame(); } catch (exception, stack) { return; } // 当帧图片就将数据封装在ImageInfo中回调出去 if (_codec!.frameCount == 1) { if (!hasListeners) { return; } _emitFrame(ImageInfo( image: _nextFrame!.image.clone(), scale: _scale, debugLabel: debugLabel, )); _nextFrame!.image.dispose(); _nextFrame = null; return; } // 多帧则继续往下走 _scheduleAppFrame(); }
通过ImageStreamListener
通知更新,刷新界面展示。
void setImage(ImageInfo image) { _checkDisposed(); _currentImage?.dispose(); _currentImage = image; if (_listeners.isEmpty) return; // Make a copy to allow for concurrent modification. final List<ImageStreamListener> localListeners = List<ImageStreamListener>.of(_listeners); for (final ImageStreamListener listener in localListeners) { try { // 设置新图篇,通知更新界面展示 listener.onImage(image.clone(), false); } catch (exception, stack) { reportError( context: ErrorDescription('by an image listener'), exception: exception, stack: stack, ); } } }
说到这里,我们好像还没提到过什么时候设置的观察者,好,我们再次回到最初的入口,_ImageState
组件的didChangeDependencies
方法中。
六、添加观察者实现界面更新
这个观察者就是通过_listenToStream
方法添加的。
@override void didChangeDependencies() { _updateInvertColors(); _resolveImage(); if (TickerMode.of(context)) // 添加观察者 _listenToStream(); else _stopListeningToStream(keepStreamAlive: true); super.didChangeDependencies(); }
并且在创建观察者ImageStreamListener
的时候,设置了onImage
的回调。
// 这里是获取观察者的入口 ImageStreamListener _getListener({bool recreateListener = false}) { if(_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( // 这个就是onImage的回调 _handleImageFrame, onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, onError: widget.errorBuilder != null || kDebugMode ? (Object error, StackTrace? stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); } : null, ); } return _imageStreamListener!; }
onImage
的入参被设置了_handleImageFrame
,因此当下载完图片后调用的就是_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; }); } void _replaceImage({required ImageInfo? info}) { _imageInfo?.dispose(); _imageInfo = info; }
到此,图片下载和更新的流程已经都串起来了。下载完的图片存放在ImageInfo
中,在setState
后,会被设置进RawImage
组件中实现渲染。
总结
网络图片的加载逻辑可以分为以下几个步骤:
1、根据图片类型,生成对应的key
2、根据key去全局的ImageCache下查找图片缓存,命中则直接返回刷新
3、图片缓存没有命中,调用Http去下载图片
4、下载完图片后,将图片的二进制数据回调出去触发界面刷新,同时会做内存缓存
5、在RawImage中显示网络图片
到此这篇关于Flutter系统网络图片加载过程解析的文章就介绍到这了,更多相关Flutter图片加载流程内容请搜索自由互联以前的文章或继续浏览下面的相关文章希望大家以后多多支持自由互联!