Flutter 图片加载方案分析之 extended_image
Flutter 默认提供了Image用于从网络、文件等加载图片,并且使用ImageCache统一管理图片缓存,但有时候并不能满足使用需求(比如网络图片没有磁盘缓存,导致每次 ImageCache 清除缓存之后又要从网络下载),所以又出现了flutter_cached_network_image、extended_image等基于 Flutter 原生的解决方案,以及power_image等基于混合开发的解决方案。
本文对 extended_image 加载过程、原理做一简单分析。
extended_image
extended_image是基于官方 Image 的拓展组件,支持加载以及失败显示,缓存网络图片,缩放拖拽图片,图片浏览 (微信掘金效果),滑动退出页面 (微信掘金效果),编辑图片 (裁剪旋转翻转),保存,绘制自定义效果等功能。
本文主要对其加载缓存网络图片的流程做一分析,因为这个库是官方 Image 的拓展,所以我们会在之前对 Image 的分析基础上进行对比分析。
extended_image 的架构图如下:
分析
因为 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_library中。
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 中
@override
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_image则是借助flutter_cache_manager实现缓存网络图片的功能,相对比较轻量。
上述两种库都是基于 Flutter Image 组件实现图片加载、缓存,阿里巴巴出品的power_image则是一款为 Flutter-Native 混合项目开发的图片加载库,借助 Texture 和 ffi 通过 Native 端已有的图片加载库完成图片加载、缓存的功能,Flutter 端只负责展示(以及 ImageCache 缓存)。