Flutter 图片库填坑记
flutter 目前流行的图片加载库主要是这 2 个:
-
- 实现很解耦,下载图片和缓存由 flutter_cache_manager 单独完成。
- pub.dev/packages/ex…_library
-
- 支持取消 http 请求和重试,用其他的库实现的,但耦合重,应该和 cached_network_image 一样剥离开。
- 还有个扩展的库 extended_image 支持手势,gallery,圆角等等各种功能,有点太重了,一些花里胡哨的功能可以删掉,图片库专注图片加载就行。
目前遇到以下问题:
- cached_network_image 在 3.19 ,ios 设备很容易 crash(尤其在老设备,测试 iphone7 滑动 1 分钟推荐流)
- extended_image 在 ios 没有发生 crash,但是在 Android 上,推荐瀑布流会越滑越掉帧。以前就是因为这个弃用了,排除法推测是下载/缓存模块有问题,可能造成了主线程任务堆积,掉帧。
临时的解决方案是 ios 用 extended_image,Android 用 cached_network_image,为了彻底解决,需要看一下 2 者的源码。
ImageProvider
flutter image 的设计很解耦,Image Widget 只负责展示图片,ImageProvider 提供解码后的图片数据,并且 flutter 的图片内存(api.flutter.dev/flutter/pai…)是自己管理的。我们想自己对 url 图片缓存,只需要自己实现一个 UrlImageProvider。
覆写核心方法 loadImage
,来把一个数据(内存,url, file 等任意文件) 转成 flutter 图片编码(ui.Codec),以及
obtainKey,根据 key 来定位当前加载的图片。
class MyImage extends ImageProvider<String>{
final String url;
@override
ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) {
// 用于图片下载进度的回调
final chunkEvents = StreamController<ImageChunkEvent>();
// 用 flutter 自带解码器 MultiFrameImageStreamCompleter, 将文件转成 ui.Image 渲染
final imageStreamCompleter = MultiFrameImageStreamCompleter(
codec: _loadImageAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: 1,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>(
'Image provider: $this \n Image key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
if (errorListener != null) {
imageStreamCompleter.addListener(
ImageStreamListener(
(image, synchronousCall) {},
onError: (Object error, StackTrace? trace) {
errorListener?.call(error);
},
),
);
}
return imageStreamCompleter;
}
@override
Future<String> obtainKey(ImageConfiguration configuration) {
return url;
}
}
修改 cached_network_image
针对上面遇到的问题,优先先看看 cached_network_image 源码,能修好就不看 extended_image_library 了。
cached_network_image 没有用 Flutter 自带的解码器 MultiFrameImageStreamCompleter
MultiFrameImageStreamCompleter({
required Future<ui.Codec> codec,
required double scale,
String? debugLabel,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
})
而是自己新写了一个
MultiImageStreamCompleter({
required Stream<ui.Codec> codec,
required double scale,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
})
他把接收的 codec 从 Future<ui.Codec>
改为了 Stream<ui.Codec>
,官方没有提供 Stream image 的解码支持,作者就 copy 了一份改了改源码,支持输入 Stream<ui.Codec>。
因为他内部使用了 flutter_cache_manager,getImageFile 返回了Stream。
mixin ImageCacheManager on BaseCacheManager {
/// Returns a resized image file to fit within maxHeight and maxWidth. It
/// tries to keep the aspect ratio. It stores the resized image by adding
/// the size to the key or url. For example when resizing
/// https://via.placeholder.com/150 to max width 100 and height 75 it will
/// store it with cacheKey resized_w100_h75_https://via.placeholder.com/150.
///
/// When the resized file is not found in the cache the original is fetched
/// from the cache or online and stored in the cache. Then it is resized
/// and returned to the caller.
Stream<FileResponse> getImageFile(
String url, {
String? key,
Map<String, String>? headers,
bool withProgress = false,
int? maxHeight,
int? maxWidth,
}) async* {
排查后发现是因为 flutter 3.19 官方 MultiFrameImageStreamCompleter 源码更新了,导致这个解码是有点问题的,长久不更新。于是首先想到的是更新一下,但是这样也不是办法,以后官方再更新呢?
不用 Stream 不行吗?这里的设计不需要 Stream,不然官方就提供了。
查看源码我们发现其实这里的 yield 没啥必要,一个 url 对应着一个 file, ImageCacheManager 下载完 file 后,这里只会调一次解码,所以这里直接改为 return 就行了。直接把 Stream 改为 Future 就可以解决了
if (result is FileInfo) {
final file = result.file;
final bytes = await file.readAsBytes();
final decoded = await decode(bytes);
yield decoded;
}
Stream<ui.Codec> _load(
String url,
String? cacheKey,
StreamController<ImageChunkEvent> chunkEvents,
Future<ui.Codec> Function(Uint8List) decode,
BaseCacheManager cacheManager,
int? maxHeight,
int? maxWidth,
Map<String, String>? headers,
ImageRenderMethodForWeb imageRenderMethodForWeb,
VoidCallback evictImage,
) async* {
try {
final stream = cacheManager is ImageCacheManager
? cacheManager.getImageFile(
url,
maxHeight: maxHeight,
maxWidth: maxWidth,
withProgress: true,
headers: headers,
key: cacheKey,
)
: cacheManager.getFileStream(
url,
withProgress: true,
headers: headers,
key: cacheKey,
);
await for (final result in stream) {
if (result is DownloadProgress) {
chunkEvents.add(
ImageChunkEvent(
cumulativeBytesLoaded: result.downloaded,
expectedTotalBytes: result.totalSize,
),
);
}
if (result is FileInfo) {
final file = result.file;
final bytes = await file.readAsBytes();
final decoded = await decode(bytes);
yield decoded;
}
}
} on Object {
// Depending on where the exception was thrown, the image cache may not
// have had a chance to track the key in the cache at all.
// Schedule a microtask to give the cache a chance to add the key.
scheduleMicrotask(() {
evictImage();
});
rethrow;
} finally {
await chunkEvents.close();
}
}
转载自:https://juejin.cn/post/7374287624545419290