跳至主要內容

Flutter 图片加载方案分析之 extended_image

JI,XIAOYONG...大约 6 分钟

Flutter 默认提供了Image用于从网络、文件等加载图片,并且使用ImageCache统一管理图片缓存,但有时候并不能满足使用需求(比如网络图片没有磁盘缓存,导致每次 ImageCache 清除缓存之后又要从网络下载),所以又出现了flutter_cached_network_imageopen in new windowextended_imageopen in new window等基于 Flutter 原生的解决方案,以及power_imageopen in new window等基于混合开发的解决方案。

本文对 extended_image 加载过程、原理做一简单分析。

extended_image

extended_imageopen in new window是基于官方 Image 的拓展组件,支持加载以及失败显示,缓存网络图片,缩放拖拽图片,图片浏览 (微信掘金效果),滑动退出页面 (微信掘金效果),编辑图片 (裁剪旋转翻转),保存,绘制自定义效果等功能。

本文主要对其加载缓存网络图片的流程做一分析,因为这个库是官方 Image 的拓展,所以我们会在之前对 Image 的分析open in new window基础上进行对比分析。

extended_image 的架构图如下:

extended_image_class_structure
extended_image_class_structure

分析

因为 extended_image 的定位是官方 Image 的拓展版,所以大部分使用方式和官方类似。

ExtendedImage

他的构造函数分别是:

  • ExtendedImage
  • ExtendedImage.asset
  • ExtendedImage.file
  • ExtendedImage.memory
  • ExtendedImage.network

同样是在构造函数中指定并创建 ImageProvider,不过 extented_image 库的 ImageProvider 都是继承自官方 ImageProvider 并且混入了ExtendedImageProvider的子类。以ExtendedImage.network为例,创建的 ImageProvider 类型是ExtendedNetworkImageProvider

其余的步骤和我们之前分析的官方 Image 组件类似,在 _ExtendedImageState 中使用 ImageProvider 获取并监听 ImageStream,当成功加载图片之后获得ImageInfo? _imageInfo并刷新页面,在_ExtendedImageState.build方法中,虽然 extended_image 增加了一些特有的加载中、加载失败、手势等封装,但最后还是使用ImageInfo.image创建ExtendedRawImage以展示图片内容。

如此可见,在从网络加载图片这部分内容来看,ExtendedImage 和 Image 的主要不同在于ExtendedNetworkImageProvider的实现:

ExtendedNetworkImageProvider

这部分内容的代码在extended_image_libraryopen in new window中。

ExtendedNetworkImageProvider 继承自ImageProvider,混入了ExtendedImageProvider,后者提供了get imageCache/instantiateImageCodec/resolveStreamForKey等一系列通用方法。

下面是 ExtendedNetworkImageProvider 的源码:

abstract class ExtendedNetworkImageProvider
    extends ImageProvider<ExtendedNetworkImageProvider>
    with ExtendedImageProvider<ExtendedNetworkImageProvider> {

 factory ExtendedNetworkImageProvider(
    String url, {
    double scale,
    Map<String, String>? headers,
    bool cache,
    int retries,
    Duration? timeLimit,
    Duration timeRetry,
    CancellationToken? cancelToken,
    String? cacheKey,
    bool printError,
    bool cacheRawData,
    String? imageCacheName,
    Duration? cacheMaxAge,
  }) = network_image.ExtendedNetworkImageProvider
}

ExtendedNetworkImageProvider 是个抽象类,他的逻辑在network_image.ExtendedNetworkImageProvider中:

import 'extended_network_image_provider.dart' as image_provider;
class ExtendedNetworkImageProvider
    extends ImageProvider<image_provider.ExtendedNetworkImageProvider>
    with ExtendedImageProvider<image_provider.ExtendedNetworkImageProvider>
    implements image_provider.ExtendedNetworkImageProvider {

    // 此方法获取的图片会被 ImageProvider 缓存到 ImageCache 中
    
    ImageStreamCompleter load(
      image_provider.ExtendedNetworkImageProvider key, DecoderCallback decode) {
    final StreamController<ImageChunkEvent> chunkEvents =
        StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(// 调用_loadAsync 方法加载图片
        key as ExtendedNetworkImageProvider,
        chunkEvents,
        decode,
      ),
      scale: key.scale,
      chunkEvents: chunkEvents.stream,
      informationCollector: () {
       ...
      },
    );
  }

    Future<ui.Codec> _loadAsync(
    ExtendedNetworkImageProvider key,
    StreamController<ImageChunkEvent> chunkEvents,
    DecoderCallback decode,
  ) async {
    assert(key == this);
    final String md5Key = cacheKey ?? keyToMd5(key.url);
    ui.Codec? result;
    if (cache) {
      try {
        // 如果需要缓存图片,就调用_loadCache 优先从缓存中读取,没有的话先从网络下载,
        // 成功之后再缓存到本地缓存文件目录
        final Uint8List? data = await _loadCache(
          key,
          chunkEvents,
          md5Key,
        );
        if (data != null) {
        // 解析加载的图片信息
          result = await instantiateImageCodec(data, decode);
        }
      } catch (e) {
        if (printError) {
          print(e);
        }
      }
    }

    if (result == null) {
      try {
        // 如果不需要缓存或者从缓存中读取/下载失败了,就从网络加载
        final Uint8List? data = await _loadNetwork(
          key,
          chunkEvents,
        );
        if (data != null) {
          result = await instantiateImageCodec(data, decode);
        }
      } catch (e) {
        if (printError) {
          print(e);
        }
      }
    }

    // 如果还是失败,就展示失败信息
    if (result == null) {
      //result = await ui.instantiateImageCodec(kTransparentImage);
      return Future<ui.Codec>.error(StateError('Failed to load $url.'));
    }

    return result;
  }
}

从上述代码可以看到,如果需要缓存时,除了 ImageCache 本身的缓存外,ExtendedNetworkImageProvider 还会执行_loadCache尝试从本地文件中读取缓存:

Future<Uint8List?> _loadCache(
    ExtendedNetworkImageProvider key,
    StreamController<ImageChunkEvent>? chunkEvents,
    String md5Key,
  ) async {
    final Directory _cacheImagesDirectory = Directory(
        join((await getTemporaryDirectory()).path, cacheImageFolderName));
    Uint8List? data;
    // 1. 先尝试从缓存文件中读取图片
    if (_cacheImagesDirectory.existsSync()) {
      final File cacheFlie = File(join(_cacheImagesDirectory.path, md5Key));
      if (cacheFlie.existsSync()) {
        if (key.cacheMaxAge != null) {
          final DateTime now = DateTime.now();
          final FileStat fs = cacheFlie.statSync();
          if (now.subtract(key.cacheMaxAge!).isAfter(fs.changed)) {
            cacheFlie.deleteSync(recursive: true);
          } else {
            data = await cacheFlie.readAsBytes();
          }
        } else {
          data = await cacheFlie.readAsBytes();
        }
      }
    }
    // create folder
    else {
      await _cacheImagesDirectory.create();
    }
    // load from network
    if (data == null) {
        // 2.1 缓存不存在或者读取失败,先仅从网络加载图片
      data = await _loadNetwork(
        key,
        chunkEvents,
      );
      if (data != null) {
        // cache image file
        // 2.2 如果从网络成功加载图片,则将图片写入文件缓存
        await File(join(_cacheImagesDirectory.path, md5Key)).writeAsBytes(data);
      }
    }

    return data;
  }

上述代码中执行到的ExtendedNetworkImageProvider._loadNetwork()方法只会使用HttpClient从网络中下载图片并返回。

ExtendedImageProvider

此外,之前提到的ExtendedImageProvider为 extended_image 库中的 ImageProvider 提供了一些通用的方法:

/// The cached raw image data 缓存图片原始数据,而不必每次都使用 ui.Image.toByteData() 获取
Map<ExtendedImageProvider<dynamic>, Uint8List> rawImageDataMap =
    <ExtendedImageProvider<dynamic>, Uint8List>{};

/// The imageCaches to store custom ImageCache,缓存 ImageCache
/// 可以指定一个 ImageCache 来缓存一些图片。这样可以一起处理它们,不会影响其他的图片缓存。
Map<String, ImageCache> imageCaches = <String, ImageCache>{};

mixin ExtendedImageProvider<T extends Object> on ImageProvider<T> {
    bool get cacheRawData;
    String? get imageCacheName;
    ImageCache get imageCache {
    if (imageCacheName != null) {
      return imageCaches.putIfAbsent(imageCacheName!, () => ImageCache());
    } else {
      return PaintingBinding.instance.imageCache;
    }
  }
}

此外,还改动了ExtendedImageProvider.resolveStreamForKey方法以使用指定的 ImageCache。

void resolveStreamForKey(
    ImageConfiguration configuration,
    ImageStream stream,
    T key,
    ImageErrorListener handleError,
  ) {
    // This is an unusual edge case where someone has told us that they found
    // the image we want before getting to this method. We should avoid calling
    // load again, but still update the image cache with LRU information.
    if (stream.completer != null) {
      final ImageStreamCompleter? completer = imageCache.putIfAbsent(
        key,
        () => stream.completer!,
        onError: handleError,
      );
      assert(identical(completer, stream.completer));
      return;
    }
    final ImageStreamCompleter? completer = imageCache.putIfAbsent(
      key,
      () => load(key, PaintingBinding.instance.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

综上所见,ExtendedImageProvider 的主要作用是借助rawImageDataMap提供了缓存图片原始数据的功能,此外还提供了一个 ImageCache 分组的方法,以便对一部分图片缓存统一处理。

总结

仅就从网络加载图片而言,extended_image 和 Flutter 官方 Image 组件的主要区别在于:在 ImageCache 之外,多了一层本地磁盘缓存,如果这二者都未命中缓存则从网络下载图片。

除此之外,extended_image 本身还提供了诸如图片缩放拖拽、滑动退出等图片操作常用的“大而全”的功能。这部分见仁见智,如果 APP 需求刚好需要用到这些功能的话,extended_image 是个不错的选择,但是如果只是想解决图片缓存问题的话,可能会显得有些臃肿。

另外一个常用的图片库flutter_cached_network_imageopen in new window则是借助flutter_cache_manageropen in new window实现缓存网络图片的功能,相对比较轻量。

上述两种库都是基于 Flutter Image 组件实现图片加载、缓存,阿里巴巴出品的power_imageopen in new window则是一款为 Flutter-Native 混合项目开发的图片加载库,借助 Texture 和 ffi 通过 Native 端已有的图片加载库完成图片加载、缓存的功能,Flutter 端只负责展示(以及 ImageCache 缓存)。

参考资料

extended_imageopen in new window

extended_image_libraryopen in new window

文章标题:《Flutter 图片加载方案分析之 extended_image》
本文作者: JI,XIAOYONG
发布时间: 2022/08/01 17:42:35 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/23ac516a.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8