Flutter系列之Image加载原理

一、前言 最近在做的项目中,总是用到Image组件,所以就了解了一下Image的源码,顺便记录下来,和大家分享一下。
本文是基于1.12.13+hotfix.8的源码,以加载网路图片为例进行解读。毕竟自己还是个小白,如果有解读不对的地方,欢迎指正。
二、Image Image继承了StatefulWidget,是用于显示图片的 Widget,最后通过内部的 RenderImage 绘制。
先看看Image结构,以Image.network为例:

Flutter系列之Image加载原理
文章图片
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
    加载图片数据源
4.1 resolve方法解析
#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
ImageStream 存储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 _pendingImages = {}; // 缓存队列 final Map _cache = {}; // 最大缓存数量,默认1000 int _maximumSize = _kDefaultSize; // 最大缓存容量,默认100 MB int _maximumSizeBytes = _kDefaultSizeBytes; ... // 省略部分代码 // 清除全部缓存 void clear() { ... } // 根据key清楚缓存 bool evict(Object key) { // ...省略代码 } //重点方法 // 参数 key用来获取缓存,loader()加载回调方法,onError加载失败回调 ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) { //_pendingImage 用于标示该key的图片处于加载中的状态 ImageStreamCompleter result = _pendingImages[key]?.completer; // 图片还未加载成功,直接返回 if (result != null) return result; // 先移除缓存,拿到移除的缓存对象 final _CachedImage image = _cache.remove(key); //把最近一次使用过的缓存在_map中 if (image != null) { _cache[key] = image; return image.completer; } //没有缓存,使用loader()方法加载 try { result = loader(); } catch (error, stackTrace) { ... } 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); // Listener is removed in [_PendingImage.removeListener]. result.addListener(streamListener); } return result; }// 当超过缓存最大数量或最大缓存容量,调用此方法清理到缓存,保持着最大数量和容量 void _checkCacheSize() { while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) { final Object key = _cache.keys.first; final _CachedImage image = _cache[key]; _currentSizeBytes -= image.sizeBytes; _cache.remove(key); } ... } }

putIfAbsent方法主要是先通过 key 判断内存中正在缓存的对象或者是否有缓存,如果有就返回该对象的ImageStreamCompleter ,否则就调用 loader 去加载并返回ImageStreamCompleter。
这里提醒大家两个地方:
  • 图片缓存是在内存中,没有进行本地存储。
  • 应用生命周期内,如果缓存没有超过上限,相同的图片(key相同)只会被下载一次。
ImageStreamCompleter putIfAbsent的返回值返回了ImageStreamCompleter,而resolve方法中,最后调用了ImageStream的setCompleter的方法,给ImageStream设置一个ImageStreamCompleter对象。
#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类型的参数——codec。Codec 是处理图片编解码的类的一个handler,是一个flutter engine API 的包装类。图片的编解码的逻辑不是在Dart 代码部分实现,而是在flutter engine中实现的。
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方法主要做了两件事:
  • 下载图片。
  • 对下载的图片数据进行解码。
下载逻辑比较简单:通过HttpClient从网上下载图片,另外下载请求会设置一些自定义的header,开发者可以通过NetworkImage的headers命名参数来传递。
在图片下载完成后调用了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(); } }

    推荐阅读