最近碰到一个问题,自己使用 AssetBundle
加载 asset 图片去绘制的时候,不能自动加载到正确分辨率下的图片。于是好奇想一探究竟—— ImageAsset
究竟做了什么,能自动适配不同分辨率的图片加载。
研究 ImageAsset
就自然要从 ImageProvider
看起,那么今天的两个问题就上线了:
我们说过带问题读源码的思路是什么?一概览,二找入口,三顺藤摸瓜对不对。
所以先从 image_provider.dart
文件看起,概览一下它有哪些类,类的大致结构怎样。
先看看文件里有哪些类
Untitled.png
看起来东西不多,还是先扫一眼,大致了解每个类的内容和作用,然后从我们的目标ImageProvider
的用法入手,一点点往里剖析。
看起来是和平台环境有关的内容,应该是用来作加载目标判定的。
const ImageConfiguration({
this.bundle,
this.devicePixelRatio,
this.locale,
this.textDirection,
this.size,
this.platform,
});
这个类的注释阿拉巴啦讲了很多,我们先不看。因为大多数人其实对 ImageProvider
特性还算了解,我们先看看它的构造,然后可以猜猜它的工作流程,我们先自己思考思考。最后再借他的注释帮我们理顺思路,查漏补缺。这样印象能更加深刻。
我们看看它的方法签名和注释。
2.1 关键方法 resolve
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.
子类应该实现 obtainKey
和 load
方法。
如果你想改变 ImageStream
的实现,重写 createStream
。
如果你要管理图片实际要使用的分辨率,重写 resolveStreamForKey
。
2.2 其它方法
这些方法我们也大致猜测一下。
// 上面提到的`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 作一些猜测
看完上面的这些方法应该能了解到这几个关键字:
这样是不是可以大致猜测出主要流程了?
入口是以 ImageConfiguration
为参数调用 ImageProvider.resolve
方法
createStream
创建 ImageStreamobtainKey
方法获取资源的 缓存键 keyresolveStreamForKey
方法load
方法加载资源分别是区分Asset资源的key,和区分尺寸的key
这几个类既是 ImageProvider
的实现类,又是缓存键类
AssetBundleImageKey
是缓存键, AssetBundleImageProvider
是抽象类,实现了读取 Asset
资源的 load
方法, ExactAssetImage
继承自 AssetBundleImageProvider
,构造方法:
const ExactAssetImage(
this.assetName, {
this.scale = 1.0,
this.bundle,
this.package,
})
有个 scale
参数,很可能和我想要的按分辨率加载相关。
我们上一节说了,关键流程在它的关键方法 resolve
里,为了展示得比较清楚,这里不得不搬运些代码了。
我这里删除了不必要的代码,只留下关键部分。如果你仔细读了上面,应该会发现这些代码一点都不陌生了。
我直接把说明写到代码注释里,看完应该就很清楚了。
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;
}
final ImageStream stream = createStream(configuration);
createStream 上面我们说过,如果你想使用不同的 ImageStream 实现,重写这个 createStream 方法就行了
这里创建了 ImageStream 实例,是我们最终要返回的结果,也是下面流程要用到的关键对象。
_createErrorHandlerAndKey(
configuration,
(T key, ImageErrorListener errorHandler) {
// 成功回调
},
(T key, dynamic exception, StackTrace stack) async {
// 失败回调
);
_createErrorHandlerAndKey
我们也说过了,用来创建 key
,同时保证无论创建 key
的方法是异步还是同步,都能捕获到异常。
他有三个参数,1. ImageConfiguration
2. key
创建成功的回调 3. key
创建失败的回调
这个方法的实现和我们猜测的一样,使用了 zone
机制,不在今天的范围内,就不描述了。
key
创建成功后走缓存策略缓存策略是在 resolveStreamForKey
方法里实现。
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);
}
}
这个方法也很简单,总共就两个分支
stream.completer
已经设置过了,那么重新往 ImageCache
里put一下load
方法获取新的 ImageStreamCompleter
方法,然后put到 ImageCache
里,再把它设置给 stream.completer
。 到这里基本就理清了,和我们当初的猜测基本一致。
再回顾一遍最初的猜测:
createStream
创建 ImageStreamobtainKey
方法获取资源的 缓存键 keyresolveStreamForKey
方法load
方法加载资源** 你可能不清楚的小知识点
如果上面有些概念你不清楚,这里稍微介绍一下:
ImageCache
是啥呢,一个图片的 LRU
缓存类, LRU
: least-recently-used
。
ImageCahce.putIfAbsent
是啥, Absent
意思是缺席、不存在,就是说如果缓存里现在没有,就put一下。当然如果有了也不是啥都不干,它会把命中的目标放到 most recently used
位置。
ImageStream
是啥,有两个成员: ImageStreamCompleter
和 List<ImageStreamListener> _listeners
。
做一件事, 设置 completer
时,会把所有已有的 listener
添加到 completer
里。
ImageStreamCompleter
又是啥,相当于观察者模式里的可订阅对象。
它又一个 ImageInfo
成员,设置这个成员时,会去通知从 ImageStream
里设置的 listener
。
在今天的场景里就是,当图片在 load
设置的加载方法中真正加载完成,会依次去通知 completer.listener
→ ImageStream.listener
→ load
方法设置的 listener
。
终于回到了最初的问题,分析思路是什么?找到入口,然后顺藤摸瓜对吧。
继承关系:
ImageProvider<AssetBundleImageKey> → AssetBundleImageProvider → AssetImage
我们上面一张提到过, ImageProvider
的实现类里,有两个必须要实现的方法 obtainKey
和 load
,其中实际在做加载图片操作的是哪个方法? load
对吧,那我们就从这个方法入手,看看它到底是做了什么,来适应不同的分辨率。
AssetImage
本身只重写了 obtainKey
方法, load
在它的父亲 AssetBundleImageProvider
里重写了。
先看看 load 方法:
// 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 方法:
@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
中做了两件事,
加载过程是第一步里做的,他用到了 key
里的两个属性, key.bundle
和 [key.name](http://key.name)
,上面说了 key
是哪来的? AssetImage
重写了 obtainKey
对不对。那我们只要看这个方法,看看这两个成员是如何赋值的就能找到答案了对不对。
先做猜测:
还是先来猜一下,这里有两个可能性,
[key.name](http://key.name)
进行了替换,自动加上了 2.0x/
或 3.0x/
之类的前缀。key.bundle
进行了替换,换成了一个拥有适配分辨率能力的 AssetBundle
。到 obtainKey 方法里找答案:
// 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
是如何赋值的,哪个更可能和分辨率有关。
最后赋值的时候两个参数 chosenBundle
和 chosenName
,前者很简单:
**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 如何赋值:
final String chosenName = _chooseVariant(
keyName,
configuration,
manifest == null ? null : manifest[keyName],
);
阅读 _chooseVariant
代码发现中确实对分辨率进行了处理,这部分就是一些计算逻辑了,我就不再罗列代码,把它的大体步骤分享一下就好:
在之前我还是先说明几个参数:
keyName
: AssetImage(keyName)
构造方法传入。
configuration
: 调用 ImageProvider.resolve
时传入,一般是使用的 widget
比如 Image
来初始化。
manifest
: pubspec.yaml
编译时生成的中间文件信息,包括你定义的图片路径等
manifest
获取对应文件所有分辨率下的路径configuration.devicePixelRatio == null
,返回原 keyName
_parseScale
,获取倍数SplayTreeMap<double, String> mapping
mapping
中,找到和 configuration.devicePixelRatio
最接近的倍数对应的路径并返回这样子,找到了正确分辨率下的图片, AssetBundleImageKey
就赋值完成。
回到 AssetBundleImageProvider._loadAsync
方法中:
data = await key.bundle.load(key.name);
是不是一下就通了呢?
今天学到了这么几点:
ImageProvider
很简单,只需要实现 load
和 obtainKey
方法rootBundle.load(path)
来加载文件,因为它并不会自动适配各类分辨率。正确的加载图片的方法是:
/// 加载图片
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 删除。