Flutter系列之Image加载原理

一、前言

最近在做的项目中,总是用到Image组件,所以就了解了一下Image的源码,顺便记录下来,和大家分享一下。
本文是基于1.12.13+hotfix.8的源码,以加载网路图片为例进行解读。毕竟自己还是个小白,如果有解读不对的地方,欢迎指正。

二、Image

Image继承了StatefulWidget,是用于显示图片的 Widget,最后通过内部的 RenderImage 绘制。
先看看Image结构,以Image.network为例:


Flutter系列之Image加载原理_第1张图片
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();
  }

七、添加磁盘缓存

上面我们已经知道,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();
    }
  }

你可能感兴趣的:(Flutter系列之Image加载原理)