LiveImage介绍
Flutter的图片缓存,是通过ImageCache类实现的,到达图片内缓存大小上限(Flutter默认是整个App的图片缓存100张和100M),就根据LRU算法回收。除此之外,Flutter在2020年初专门设计了一个LiveImage缓存。这个LiveImage缓存是在原有的image cache之外的,不受缓存大小限制。
LiveImage是为了fix 3个issue:
#48731
#49456
#45406\
简要说,就是当Image widge仍在WigetTree时,不能因为到达设定的图片内存缓存上限,而被清除。源码的主要设计是:
1、_ImageState的didChangeDependencies根据状态增加和停止对ImageStream的listener(keepStreamAlive参数很有用)
@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();
if (TickerMode.of(context))
_listenToStream();
else
_stopListeningToStream(keepStreamAlive: true);
super.didChangeDependencies();
}
2、ImageStreamCompleter增加一个回调addOnLastListenerRemovedCallback,LiveImage在listener全部移除后,移除缓存,缓存以map的结构,存在于ImageCache的属性_liveImages
class _LiveImage extends _CachedImageBase {
_LiveImage(ImageStreamCompleter completer, VoidCallback handleRemove, {int? sizeBytes})
: super(completer, sizeBytes: sizeBytes) {
_handleRemove = () {
handleRemove();
dispose();
};
completer.addOnLastListenerRemovedCallback(_handleRemove);
}
late VoidCallback _handleRemove;
@override
void dispose() {
completer.removeOnLastListenerRemovedCallback(_handleRemove);
super.dispose();
}
@override
String toString() => describeIdentity(this);
}
问题
假定有2个page:A(普通页面)、B(商品列表页)
因为LiveImage仅追踪当前页面,打开A,加载图片,图片会同时缓存到image cache和LiveImage,A push B,A页面的图片在LiveImage的缓存,会被清除,但image cache还有缓存。B持续滚动,加载了超多的商品图,这时,会导致A页面使用的图片缓存被回收。然后pop B回到A,A页面的图片会重新加载。
重现视频:user-images.githubusercontent.com/2723525/147…
因为_stopListeningToStream有keepStreamAlive,本来Image widget的state是持有image stream的,就是缓存被清,Image widget自己还持有,应该不用重新加载。但是这里有一个bug,_ImageState的didChangeDependencies会执行updateStream,自己把已有的ImageStream干掉了,只能重新加载。
具体我在github提了2个issue:
_liveImages only track widget tree for current page
keepStreamAlive not working
这对电商App是影响很大的,用户在商品列表滑动,很轻易就能超100张,那么继续滑动加载图片,App进程内,之前页面(是之前的所有页面)的图片缓存就逐渐被清除了,再回去页面的图片都重新加载,太不能接受了。
解决
改Flutter源码
那么2个思路,都是修改_ImageState:
1、删除didChangeDependencies的_stopListeningToStream,只在dispose方法调用stop,这样LiveImage就不仅仅追踪当前页面,而是整个App的live image
2、_resolveImage方法增加一个判断,这样避免因为_stopListeningToStream有keepStreamAlive的ImageStream被重置
if (_completerHandle) {
return;
}
不改Flutter源码
那么思路是:想办法增加一个对ImageStream的listener,这样避免removeOnLastListenerRemovedCallback过早的回调,同样,LiveImage就不仅仅追踪当前页面,而是整个App的live image
比如我这里,自定义一个CustomImageProvider,将ImageStream抛出来,用一个自定义的ImageWarpper接收,增加一个listener,并在dispose时移除listener
ps: 1、无法在CustomImageProvider通过override createStream方法来抛出ImageStream,因为在_imageState的_resolveImage方法内,provider都会被wrap一个ScrollAwareImageProvider(这个provider是专门为了优化快速滑动时,取消还没来及加载就滑出屏幕的Image),所以自己写的provider根本没机会被调用createStream,其实是所有的都没机会调用,之所以resolveStreamForKey能执行,是因为ScrollAwareImageProvider做了转发。真是偷懒,多写个转发啊。2、为了避免重复build导致缓存出问题,需要通过Stream的key判断
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class ImageWarpper extends StatefulWidget {
final String url;
const ImageWarpper({Key? key, required this.url}) : super(key: key);
@override
State<ImageWarpper> createState() => _ImageWarpperState();
}
class _ImageWarpperState extends State<ImageWarpper> {
ImageStream? _imageStream;
@override
void initState() {
if (kDebugMode) {
// print("[ImageWarpper] init image: ${widget.url}");
}
super.initState();
}
@override
void dispose() {
if (kDebugMode) {
// print("[ImageWarpper] dispose image: ${widget.url}");
}
_stopListeningToStream();
super.dispose();
}
ImageStreamListener? _imageStreamListener;
ImageStreamListener _getListener({bool recreateListener = false}) {
_imageStreamListener ??= ImageStreamListener(
(ImageInfo imageInfo, bool synchronousCall) {},
);
return _imageStreamListener!;
}
_stopListeningToStream() {
_imageStream?.removeListener(_getListener());
}
@override
Widget build(BuildContext context) {
return Image(
image: CustomImageProvider(
imageProvider: NetworkImage(widget.url),
onResolveStreamForKey: (ImageConfiguration configuration,
ImageStream stream, dynamic key, ImageErrorListener handleError) {
print("onResolveStreamForKey");
if (_imageStream == null) {
_imageStream = stream;
stream.addListener(_getListener(recreateListener: true));
return;
}
if (_imageStream?.key != stream.key) {
// stream不一样,重置
_stopListeningToStream();
_imageStream = stream;
stream.addListener(_getListener(recreateListener: true));
} else {
// 避免重复build有问题,do nothing
}
}),
height: 100,
fit: BoxFit.cover,
);
}
}
typedef OnResolveStreamForKey = Function(ImageConfiguration configuration,
ImageStream stream, dynamic key, ImageErrorListener handleError);
@optionalTypeArgs
class CustomImageProvider<T extends Object> extends ImageProvider<T> {
const CustomImageProvider(
{required this.imageProvider, this.onResolveStreamForKey})
: assert(imageProvider != null);
/// The wrapped image provider to delegate [obtainKey] and [load] and [resolveStreamForKey] to.
final ImageProvider<T> imageProvider;
final OnResolveStreamForKey? onResolveStreamForKey;
@override
void resolveStreamForKey(
ImageConfiguration configuration,
ImageStream stream,
T key,
ImageErrorListener handleError,
) {
imageProvider.resolveStreamForKey(configuration, stream, key, handleError);
// 下面的代码必须后执行,才能让stream带上key
if (onResolveStreamForKey != null) {
onResolveStreamForKey!(configuration, stream, key, handleError);
}
}
@override
ImageStreamCompleter load(T key, DecoderCallback decode) =>
imageProvider.load(key, decode);
@override
Future<T> obtainKey(ImageConfiguration configuration) =>
imageProvider.obtainKey(configuration);
}
后续
1、我不知道为什么Flutter原本的LiveImage设计,只追踪当前页面,原生不是这样的,iOS的SDWebImage,也是18年,通过强弱引用的数据结构,专门处理了UIImageView持有的图片内存,保障缓存不被乱清
2、LiveImage独立用一个map,导致图片缓存的大小设置,等同虚设了,不应该啊
文章有错误的地方请指出(错误太多我就删了:)