【Flutter】源码分析之Image加载过程解析

2,610 阅读5分钟

前言

Flutter的Image组件由四种加载形式:network、file、asset、memory,四种方式分别为网络、文件格式、asset资源、Uint8List格式数据。下面以通过Image.network()形式深入了解Flutter如何实现对图片加载和渲染。

提前知道点什么

ImageProvider

它的主要职责是提供数据资源、图片缓存。ResizeImage、NetworkImage、FileImage等都是对它的具体实现,根据不同数据源在load()实现图片加载方式。

  • resolve(返回ImageStream监听图片加载过程)
  • evict(图片缓存相关)
  • load(图片加载方法,由子类具体实现)

PaintingBinding

PaintingBinding是全局单例,包含了ImageCache成员对象。在ImageProvider的resolve方法中如果对于图片对象key存在缓存会从ImageCache中直接读取对象;在获取图片资源Uint8List数据后通过instantiateImageCodec创建编解码器。所以PaintingBinding在图片加载过程中主要提供图片缓存和图片资源编解码的作用。

  • instantiateImageCodec
  • PaintingBinding.instance.imageCache

MultiFrameImageStreamCompleter

MultiFrameImageStreamCompleter负责chunkEvents(资源加载情况)和FrameInfo(图片帧)。如果是gif动图调用_scheduleAppFrame()循环获取_nextFrame.image绘制。使用_emitFrame()将ImageInfo通过ImageStream注册的ImageStreamListener监听接口传递到State。

  • _handleCodecReady
  • _decodeNextFrameAndSchedule
  • _handleAppFrame(gif图片调用)

Codec

Codec是图片编解码句柄,主要提供了FlutterEngine底层获取图片帧数据接口。所以这里实现图片编解码的过程并非在Dart层级。

  • getNextFrame(获取图片帧)
  • instantiateImageCodec(获取解码器)

网络图片加载过程

  1. 网络图片加载过程是在NetwrokImage调用load()方法时实例化MultiFrameImageStreamCompleter通过调用_loadAsync()方法。

  2. _loadAsync()通过静态HttpClient通过图片地址Url下载图片资源,并将图片资源数据以Uint8List返回,这个过程由PaintingBinding单例调用引擎层_instantiateImageCodec()实现。

  3. 在下载过程中会通过StreamController获取进度,信息包含:cumulativeBytesLoaded(已加载大小)、expectedTotalBytes(预计总大小)。

  4. 经过引擎层图片数据处理返回ui.Codec编解码器给到MultiFrameImageStreamCompleter然后由ImageProvider获取到ImageStreamCompleter。

  5. 图片加载执行方法会在_ImageState的生命周期中执行,通过调用ImageProvider的resolve()方法获取到ImageStream。

  6. ImageProvider方法resolve()过程中会通过ImageConfiguration配置生成Key,通过key在PaintingBinding单例的ImageCache中查找图片缓存。

  7. 若存在图片缓存对象的ImageCompleter直接获取,不存在则需要执行ImageProvider的load()方法先加载到图片资源。

图片渲染过程

  1. 图片渲染过程发生在Image组件的_ImageState中。在特定生命周期时会调用_resolverImage() 获取ImageStream更新ImageStream资源并设置ImageStreamListener监听。

  2. 在调用_resolverImage()同时调用了ImageProvider的resolve()方法执行图片加载过程。

  3. 图片资源加载过程会通过_handleImageFrame()回调方法返回,返回对象重要对象为ImageInfo包含了图片的宽高、缩放比、图片数据ByteData

  4. 最终图片渲染由RawImage实现,其内部由RenderImage通过paintImage()在画布上绘制图片。

内容补充

version-1.17.0更新ImageCache

Flutter最新更新了1.17.0其中ImageCache新增了_liveImages。_liveImages主要作用前置常用图片缓存,当ImageCache缓存池优先清除之前的缓存图片可以采用liveImages缓存队列中的图片数据,通过ImageStream的removeListener方法可以了解它的生命周期所以适用于当前页面,退出当前页面会从缓存中清除。(个人源码阅读理解,若有误请补充说明)

前版本源代码

  • 原先缓存逻辑是图片缓存先放在_PendingImage中,当获取到图片数据后移除PendingImage缓存将缓存添加到_cacheImage中。
  • 获取缓存优先级:PendingImage -> cacheImage -> load()
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
    // 直接从_pendingImages缓存中获取(可能图片还在加载中)
    ImageStreamCompleter result = _pendingImages[key]?.completer;
    if (result != null)
      return result;
    // 若为空则从cache中获取
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      _cache[key] = image;
      return image.completer;
    }
    // 如果缓存都为空则调用Provider的loader方法加载图片数据
    try {
      result = loader();
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    /// ImageStreamCompleter回调函数
    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);
      result.addListener(streamListener);
    }
    return result;
  }

新版本源代码

  • 新版本主要是新增了_trackLiveImage()、_touch()两个方法。在原先 _pendingImages和_cache基础上新增_liveImages缓存队列。
  • _touch()执行缓存大小判断,将图片缓存放入_cache缓存队列。
  • _trackLiveImage()为liveImage设置图片sizeBytes并添加回调清除图片缓存接口。
  • 获取缓存优先级:pendingImage -> cacheImage -> liveImage -> load()
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
final Map<Object, _LiveImage> _liveImages = <Object, _LiveImage>{};

ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
    ..... 省略相同部分代码
    final _CachedImage image = _cache.remove(key);
    if (image != null) {
      /// 缓存添加图片后再向liveImage添加
      _trackLiveImage(key, _LiveImage(image.completer, image.sizeBytes, () => _liveImages.remove(key)));
      _cache[key] = image;
      return image.completer;
    }
    /// 若缓存中不存在则从liveImage缓存队列查找
    final _CachedImage liveImage = _liveImages[key];
    if (liveImage != null) {
      _touch(key, liveImage, timelineTask);
      return liveImage.completer;
    }
    /// 如果liveImages缓存中也没有则调用loader()加载图片资源。
    try {
      result = loader();
      _trackLiveImage(key, _LiveImage(result, null, () => _liveImages.remove(key)));
    } catch (error, stackTrace) {
      if (onError != null) {
        onError(error, stackTrace);
        return null;
      } else {
        rethrow;
      }
    }
    /// 若缓存被禁用了 使用该变量放置图片数据
    _PendingImage untrackedPendingImage;
    /// 和原先回调方式相同,ImageStreamCompleter回调函数设置,回调后将图片从_PendingImage缓存队列中移除。
    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);
      _trackLiveImage(
        key,
        _LiveImage(
          result,
          imageSize,
          () => _liveImages.remove(key),
        ),
        debugPutOk: syncCall,
      );

      final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
      if (pendingImage != null) {
        pendingImage.removeListener();
      }
      if (untrackedPendingImage == null) {
        _touch(key, image, listenerTask);
      }
    }
    final ImageStreamListener streamListener = ImageStreamListener(listener);
    /// 修改了原有增加缓存的逻辑 通过untrackedPendingImage不缓存图片。
    if (maximumSize > 0 && maximumSizeBytes > 0) {
      _pendingImages[key] = _PendingImage(result, streamListener);
    } else {
      untrackedPendingImage = _PendingImage(result, streamListener);
    }
    result.addListener(streamListener);
}
 /// liveImage预先加载
 void _trackLiveImage(Object key, _LiveImage image, { bool debugPutOk = true }) {
    _liveImages.putIfAbsent(key, () {
      image.completer.addOnLastListenerRemovedCallback(image.handleRemove);
      return image;
    }).sizeBytes ??= image.sizeBytes;
  }
  /// liveImage加载后并添加到_cache中
  void _touch(Object key, _CachedImage image, TimelineTask timelineTask) {
    if (image.sizeBytes != null && image.sizeBytes <= maximumSizeBytes) {
      _currentSizeBytes += image.sizeBytes;
      _cache[key] = image;
      _checkCacheSize(timelineTask);
    }
  }  

总结

最后通过一张图总结Image加载过程,主要通过ImageProvider获取到图片资源将ImageStream给到ImageState。ImageState可通过ImageStream监听图片的加载进度,最后获取ImageInfo后通过RawImage绘制图片。

参考