前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 学习:ImageProvider工作流程和AssetImage 的自动分辨率适配原理

Flutter 学习:ImageProvider工作流程和AssetImage 的自动分辨率适配原理

原创
作者头像
用户1468672
修改2020-11-16 09:52:17
6.7K0
修改2020-11-16 09:52:17
举报

最近碰到一个问题,自己使用 AssetBundle 加载 asset 图片去绘制的时候,不能自动加载到正确分辨率下的图片。于是好奇想一探究竟—— ImageAsset 究竟做了什么,能自动适配不同分辨率的图片加载。

研究 ImageAsset 就自然要从 ImageProvider 看起,那么今天的两个问题就上线了:

  1. ImageProvider 的图片加载流程
  2. ImageAsset 如何做到不同分辨率的适配

我们说过带问题读源码的思路是什么?一概览,二找入口,三顺藤摸瓜对不对。

所以先从 image_provider.dart 文件看起,概览一下它有哪些类,类的大致结构怎样。

一、类的结构

先看看文件里有哪些类

Untitled.png

image.png
image.png
  • ImageConfiguration
  • ImageProvider<T> 抽象基类
  • Key 系
  • ImageProvider 系
  • 其它

看起来东西不多,还是先扫一眼,大致了解每个类的内容和作用,然后从我们的目标ImageProvider的用法入手,一点点往里剖析。

1. ImageConfiguration

看起来是和平台环境有关的内容,应该是用来作加载目标判定的。

代码语言:txt
复制
const ImageConfiguration({
  this.bundle,
  this.devicePixelRatio,
  this.locale,
  this.textDirection,
  this.size,
  this.platform,
});

2. ImageProvider<T>抽象基类

这个类的注释阿拉巴啦讲了很多,我们先不看。因为大多数人其实对 ImageProvider 特性还算了解,我们先看看它的构造,然后可以猜猜它的工作流程,我们先自己思考思考。最后再借他的注释帮我们理顺思路,查漏补缺。这样印象能更加深刻。

我们看看它的方法签名和注释。

2.1 关键方法 resolve

代码语言:txt
复制
ImageStream resolve(ImageConfiguration configuration);

This is the public entry-point of the ImageProvider class hierarchy.

注释说,这个方法是 ImageProvider 家族的public的入口,返回值是 ImageStream 。就是说所有的 ImageProvider 都是调这个方法来加载图片流。

既然这个方法是入口,主要流程应该都在这个方法里。一会儿我们来主要分析这个方法。

继续看注释:

Subclasses should implement obtainKey and load, which are used by this method. If they need to change the implementation of ImageStream used,

they should override createStream. If they need to manage the actual

resolution of the image, they should override resolveStreamForKey.

子类应该实现 obtainKeyload 方法。

如果你想改变 ImageStream 的实现,重写 createStream

如果你要管理图片实际要使用的分辨率,重写 resolveStreamForKey

2.2 其它方法

这些方法我们也大致猜测一下。

代码语言:txt
复制
// 上面提到的`createStream`方法
ImageStream createStream(ImageConfiguration configuration);

// 缓存相关
Future<ImageCacheStatus> obtainCacheStatus({
    @required ImageConfiguration configuration,
    ImageErrorListener handleError,
  })

// 和异常捕获相关,注释说用来保证捕获创建key期间的所有一场,「包括同步和异步」。大概率会用到zone相关的内容。
_createErrorHandlerAndKey(
    ImageConfiguration configuration,
    _KeyAndErrorHandlerCallback<T> successCallback,
    _AsyncKeyErrorHandler<T> errorCallback,
  )

// 根据key获取stream,提到来key,想必是和缓存相关了
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError);

// evict[驱逐] 带ImageCache参数,应当是从缓存里移除之类的
Future<bool> evict({ ImageCache cache, ImageConfiguration configuration = ImageConfiguration.empty });

// 这俩是实现 [ImageProvider] 必须实现的方法,应该是获取 key 和加载流的关键方法了。
Future<T> obtainKey(ImageConfiguration configuration);
ImageStreamCompleter load(T key, DecoderCallback decode);

2.3 作一些猜测

看完上面的这些方法应该能了解到这几个关键字:

  1. ImageConfiguration 平台环境参数
  2. ImageStream 最终返回的图片数据流
  3. key 大概率是缓存键
  4. 必须实现 load方法和obtainKey方法

这样是不是可以大致猜测出主要流程了?

入口是以 ImageConfiguration 为参数调用 ImageProvider.resolve 方法

  1. 调用 createStream 创建 ImageStream
  2. 调用 obtainKey 方法获取资源的 缓存键 key
  3. 以 key 和 stream 为参数调用 resolveStreamForKey 方法
    1. 去缓存中查询是否有key对应的缓存
    2. 若有缓存,使用缓存
    3. 若无缓存,调用 load 方法加载资源

3. ResizeImage/_SizeAwareCacheKey

分别是区分Asset资源的key,和区分尺寸的key

4. NetworkImage/FileImage/MemoryImage

这几个类既是 ImageProvider 的实现类,又是缓存键类

5. AssetBundleImageKey/AssetBundleImageProvider/ExactAssetImage

AssetBundleImageKey 是缓存键, AssetBundleImageProvider 是抽象类,实现了读取 Asset 资源的 load 方法, ExactAssetImage 继承自 AssetBundleImageProvider ,构造方法:

代码语言:txt
复制
const ExactAssetImage(
  this.assetName, {
  this.scale = 1.0,
  this.bundle,
  this.package,
})

有个 scale 参数,很可能和我想要的按分辨率加载相关。

二、ImageProvider 的主要工作流程分析

我们上一节说了,关键流程在它的关键方法 resolve 里,为了展示得比较清楚,这里不得不搬运些代码了。

我这里删除了不必要的代码,只留下关键部分。如果你仔细读了上面,应该会发现这些代码一点都不陌生了。

我直接把说明写到代码注释里,看完应该就很清楚了。

代码语言:txt
复制
ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
		// [1]
    final ImageStream stream = createStream(configuration);
		
		// [2]
    _createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {

				// [3]
        resolveStreamForKey(configuration, stream, key, errorHandler);

      },
      (T key, dynamic exception, StackTrace stack) async {
        // key 创建失败的处理,不是关键
    );
    return stream;
  }
  1. 创建 ImageStream
代码语言:txt
复制
final ImageStream stream = createStream(configuration);

createStream 上面我们说过,如果你想使用不同的 ImageStream 实现,重写这个 createStream 方法就行了

这里创建了 ImageStream 实例,是我们最终要返回的结果,也是下面流程要用到的关键对象。

  1. 创建缓存键 key
代码语言:txt
复制
_createErrorHandlerAndKey(
      configuration,
      (T key, ImageErrorListener errorHandler) {
			// 成功回调
      },
      (T key, dynamic exception, StackTrace stack) async {
      // 失败回调
    );

_createErrorHandlerAndKey 我们也说过了,用来创建 key,同时保证无论创建 key的方法是异步还是同步,都能捕获到异常。

他有三个参数,1. ImageConfiguration 2. key创建成功的回调 3. key创建失败的回调

这个方法的实现和我们猜测的一样,使用了 zone 机制,不在今天的范围内,就不描述了。

  1. key 创建成功后走缓存策略

缓存策略是在 resolveStreamForKey 方法里实现。

代码语言:txt
复制
void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, T key, ImageErrorListener handleError) {
    if (stream.completer != null) {
			// 分支 1
      final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
        key,
        () => stream.completer,
        onError: handleError,
      );
      assert(identical(completer, stream.completer));
      return;
    }
		// 分支 2
    final ImageStreamCompleter completer = PaintingBinding.instance.imageCache.putIfAbsent(
      key,
      () => load(key, PaintingBinding.instance.instantiateImageCodec),
      onError: handleError,
    );
    if (completer != null) {
      stream.setCompleter(completer);
    }
  }

这个方法也很简单,总共就两个分支

  1. 如果 stream.completer 已经设置过了,那么重新往 ImageCache 里put一下
  2. 如果没设置过,调 load 方法获取新的 ImageStreamCompleter 方法,然后put到 ImageCache 里,再把它设置给 stream.completer

到这里基本就理清了,和我们当初的猜测基本一致。

再回顾一遍最初的猜测:

  1. 调用 createStream 创建 ImageStream
  2. 调用 obtainKey 方法获取资源的 缓存键 key
  3. 以 key 和 stream 为参数调用 resolveStreamForKey 方法
    1. 去缓存中查询是否有key对应的缓存
    2. 若有缓存,使用缓存
    3. 若无缓存,调用 load 方法加载资源

** 你可能不清楚的小知识点

如果上面有些概念你不清楚,这里稍微介绍一下:

ImageCache 是啥呢,一个图片的 LRU 缓存类, LRUleast-recently-used

ImageCahce.putIfAbsent 是啥, Absent 意思是缺席、不存在,就是说如果缓存里现在没有,就put一下。当然如果有了也不是啥都不干,它会把命中的目标放到 most recently used 位置。

ImageStream 是啥,有两个成员: ImageStreamCompleterList<ImageStreamListener> _listeners

做一件事, 设置 completer 时,会把所有已有的 listener 添加到 completer 里。

ImageStreamCompleter 又是啥,相当于观察者模式里的可订阅对象。

它又一个 ImageInfo 成员,设置这个成员时,会去通知从 ImageStream 里设置的 listener

在今天的场景里就是,当图片在 load 设置的加载方法中真正加载完成,会依次去通知 completer.listenerImageStream.listenerload 方法设置的 listener

三、AssetImage 如何自动适配不同分辨率加载图片?

终于回到了最初的问题,分析思路是什么?找到入口,然后顺藤摸瓜对吧。

继承关系:

ImageProvider<AssetBundleImageKey> → AssetBundleImageProvider → AssetImage

我们上面一张提到过, ImageProvider 的实现类里,有两个必须要实现的方法 obtainKeyload ,其中实际在做加载图片操作的是哪个方法? load 对吧,那我们就从这个方法入手,看看它到底是做了什么,来适应不同的分辨率。

AssetImage 本身只重写了 obtainKey 方法, load 在它的父亲 AssetBundleImageProvider 里重写了。

先看看 load 方法:

代码语言:txt
复制
// class AssetBundleImageProvider
@override
ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
  InformationCollector collector;
  return MultiFrameImageStreamCompleter(
    codec: _loadAsync(key, decode),
    scale: key.scale,
    informationCollector: collector
  );
}

可以看到 load 方法返回了一个 MultiFrameImageStreamCompleter 实例,这个类的构造方法中调用了 codec.then(xxx),也就是 _loadAsync 方法。

_loadAsync 方法:

代码语言:txt
复制
@protected
  Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
    ByteData data;
    try {
      data = await key.bundle.load(key.name);
    } on FlutterError {
      // xxxxx
    }
    if (data == null) {
			// xxxxx
    }
    return await decode(data.buffer.asUint8List());
  }

_loadAsync 中做了两件事,

  1. key.bundle.load(key.name);
  2. decode(data.buffer.asUint8List());

加载过程是第一步里做的,他用到了 key 里的两个属性, key.bundle[key.name](http://key.name) ,上面说了 key 是哪来的? AssetImage 重写了 obtainKey 对不对。那我们只要看这个方法,看看这两个成员是如何赋值的就能找到答案了对不对。

先做猜测:

还是先来猜一下,这里有两个可能性,

  1. 方法里对 [key.name](http://key.name) 进行了替换,自动加上了 2.0x/3.0x/ 之类的前缀。
  2. 方法里对 key.bundle 进行了替换,换成了一个拥有适配分辨率能力的 AssetBundle

到 obtainKey 方法里找答案:

代码语言:txt
复制
// class AssetImage

Future<AssetBundleImageKey> obtainKey(ImageConfiguration configuration) {
    **final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;
		// xxxxx**

    chosenBundle.loadStructuredData<Map<String, List<String>>>(_kAssetManifestFileName, _manifestParser).then<void>(
      (Map<String, List<String>> manifest) {
        **final String chosenName = _chooseVariant(
          keyName,
          configuration,
          manifest == null ? null : manifest[keyName],
        );**
        final double chosenScale = _parseScale(chosenName);
        final AssetBundleImageKey key = AssetBundleImageKey(
          **bundle: chosenBundle,
          name: chosenName,**
          scale: chosenScale,
        );
        // 分发结果 xxxxx
      }
    ).catchError((dynamic error, StackTrace stack) {
      // 处理错误 xxxxx
    });
		// 返回结果 xxxxx
  }

我把关键部分加粗了,回忆一下我们的目的是什么?找到 [key.name](http://key.name)key.bundle 是如何赋值的,哪个更可能和分辨率有关。

最后赋值的时候两个参数 chosenBundlechosenName ,前者很简单:

代码语言:txt
复制
**final AssetBundle chosenBundle = bundle ?? configuration.bundle ?? rootBundle;**

结果会依次从这三个候选参数中选择, bundle 是实例化 AssetBundle 作为参数传入的,我们知道不传这个参数,对适配没有影响,可以排除。

configuration.bundle 是调用 ImageProvider.resolve(ImageConfiguration) 时传入的,一般这个使用 DefaultAssetBundle.of(context), 一般来说它也会返回 rootBundle ,我们知道 rootBundle 本身没有适配分辨率的能力。

基于此,基本可以排除第二个猜测——包装了一个适配分辨率的 AssetBundle ——是错误的。

那么可能性就是第一个猜测了——方法里对 [key.name](http://key.name) 进行了替换,自动加上了 2.0x/3.0x/ 之类的前缀。

chosenName 如何赋值:

代码语言:txt
复制
final String chosenName = _chooseVariant(
    keyName,
    configuration,
    manifest == null ? null : manifest[keyName],
  );

阅读 _chooseVariant 代码发现中确实对分辨率进行了处理,这部分就是一些计算逻辑了,我就不再罗列代码,把它的大体步骤分享一下就好:

在之前我还是先说明几个参数:

keyNameAssetImage(keyName) 构造方法传入。

configuration: 调用 ImageProvider.resolve 时传入,一般是使用的 widget比如 Image 来初始化。

manifest : pubspec.yaml 编译时生成的中间文件信息,包括你定义的图片路径等

  1. manifest 获取对应文件所有分辨率下的路径
  2. 如果获取到的路径为空或 configuration.devicePixelRatio == null ,返回原 keyName
  3. 遍历路径列表
    1. 从路径中 _parseScale ,获取倍数
    2. 以倍数为键,路径为值,存入 SplayTreeMap<double, String> mapping
  4. mapping 中,找到和 configuration.devicePixelRatio 最接近的倍数对应的路径并返回
    1. 寻找规则是就近规则,和安卓系统的规则相同

这样子,找到了正确分辨率下的图片, AssetBundleImageKey 就赋值完成。

回到 AssetBundleImageProvider._loadAsync 方法中:

代码语言:txt
复制
data = await key.bundle.load(key.name);

是不是一下就通了呢?

四、总结

今天学到了这么几点:

  1. 实现一个 ImageProvider 很简单,只需要实现 loadobtainKey 方法
  2. 不要再简单地使用 rootBundle.load(path) 来加载文件,因为它并不会自动适配各类分辨率。

正确的加载图片的方法是:

代码语言:txt
复制
/// 加载图片
static Future<ui.Image> _loadImage(BuildContext context, String path) async {
  Completer<ui.Image> completer = Completer();

	AssetImage(path)
      .resolve(createLocalImageConfiguration(context))
      .addListener(ImageStreamListener((image, _) {
        completer.complete(image.image);
      }, onError: (_, __) {}));
  return completer.future;
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、类的结构
    • 1. ImageConfiguration
      • 2. ImageProvider<T>抽象基类
        • 3. ResizeImage/_SizeAwareCacheKey
          • 4. NetworkImage/FileImage/MemoryImage
            • 5. AssetBundleImageKey/AssetBundleImageProvider/ExactAssetImage
            • 二、ImageProvider 的主要工作流程分析
            • 三、AssetImage 如何自动适配不同分辨率加载图片?
            • 四、总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档