简介
Flutter
提供的图片展示工具没有缓存功能,这个插件填补了空白。在Pub
上,这个插件的评价也是很高的。
缓存
这个插件的缓存,调用了另外一个插件flutter_cache_manager
关于这一点,在cached_network_image
的介绍中有提到,不过基本上很少有人注意。
大多数时候,感觉缓存应该是有的,访问过的大图,再次访问速度会比较快。只是平时用的时候根本不会去关心缓存的事情。基本上会按照官网介绍的用:
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
大部分时候,就是把placeholder
换成一个本地图片,至于缓存,基本上不会在意。
由缓存导致的问题
我们的是电商应用,所以图片较多,对
CachedNetworkImage
封装了之后,大量应用封装也是简单套了一层,提供了一张本地的静态图片作为
placeholder
,然后errorWidget
和placeholder
设置成了一样
class NetworkImageWidget extends StatelessWidget {
/// ... ...
return CachedNetworkImage(
imageUrl: actualUrl,
placeholder: (context, url) => _buildPlaceholderImage(actualPlaceholder),
errorWidget: (context, url, error) => _buildPlaceholderImage(actualPlaceholder),
width: width,
height: height,
);
}
几乎所有的图片都调用这个封装之后的控件,使用下来,体验还是很不错的。
我们的应用,提供商品的照片,为了照片质量,都是用
iPhone12
拍摄的,采用的是默认的4032*3024
分辨率,上传到阿里cnd
之后,一张照片的大小有2M
左右;后来,用户反馈说
APP
会崩溃,特别是在查看商品的高清照片时更容易发生。连上手机折腾,最后发现,是
CachedNetworkImage
占用内存太大(印象中是2G
的样子),导致APP
退出,现象和崩溃差不多……
怎么会这样?怎么解决?在线等,挺急的……
默认缓存背锅
从效果来看,肯定是有缓存的,
我们封装的组件并没有指定缓存,那么组件内部使用了默认的缓存
CachedNetworkImage
也是一个壳,内部真正起作用的是CachedNetworkImageProvider
;官网文档也提到用这两个作用是一样的,确实是这样。
CachedNetworkImage({
Key? key,
required this.imageUrl,
this.httpHeaders,
this.imageBuilder,
this.placeholder,
this.progressIndicatorBuilder,
this.errorWidget,
this.fadeOutDuration = const Duration(milliseconds: 1000),
this.fadeOutCurve = Curves.easeOut,
this.fadeInDuration = const Duration(milliseconds: 500),
this.fadeInCurve = Curves.easeIn,
this.width,
this.height,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.matchTextDirection = false,
this.cacheManager,
this.useOldImageOnUrlChange = false,
this.color,
this.filterQuality = FilterQuality.low,
this.colorBlendMode,
this.placeholderFadeInDuration,
this.memCacheWidth,
this.memCacheHeight,
this.cacheKey,
this.maxWidthDiskCache,
this.maxHeightDiskCache,
ImageRenderMethodForWeb imageRenderMethodForWeb =
ImageRenderMethodForWeb.HtmlImage,
}) : _image = CachedNetworkImageProvider(
imageUrl,
headers: httpHeaders,
cacheManager: cacheManager,
cacheKey: cacheKey,
imageRenderMethodForWeb: imageRenderMethodForWeb,
maxWidth: maxWidthDiskCache,
maxHeight: maxHeightDiskCache,
),
super(key: key);
缓存参数
cacheManager
,也被传给了CachedNetworkImageProvider
- 继续往下走,看
CachedNetworkImageProvider
做了什么。从下面这段代码可以看出,缓存参数cacheManager
用在了下载过程。如果不指定,就用默认的DefaultCacheManager()
Stream<ui.Codec> _loadBufferAsync(
image_provider.CachedNetworkImageProvider key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderBufferCallback decode,
) {
assert(key == this);
return ImageLoader().loadBufferAsync(
url,
cacheKey,
chunkEvents,
decode,
cacheManager ?? DefaultCacheManager(),
maxHeight,
maxWidth,
headers,
errorListener,
imageRenderMethodForWeb,
() => PaintingBinding.instance.imageCache.evict(key),
);
}
- 到这里,问题算搞清楚了一半:确实有一个默认的缓存,所以平时不需要指定缓存。那么反过来,内存占用过大基本上也是这个默认缓存
DefaultCacheManager()
造成的。继续往下看:
/// The DefaultCacheManager that can be easily used directly. The code of
/// this implementation can be used as inspiration for more complex cache
/// managers.
class DefaultCacheManager extends CacheManager with ImageCacheManager {
static const key = 'libCachedImageData';
static final DefaultCacheManager _instance = DefaultCacheManager._();
factory DefaultCacheManager() {
return _instance;
}
DefaultCacheManager._() : super(Config(key));
}
就是简单地把插件
flutter_cache_manager
封装成了单例,什么也没干,确实够懒的。注释也算实诚,“可以直接用,也可以提供一个灵感……”,这话讲的,真不好评价
-
Config(key
)结构就是配置缓存参数的地方
import '_config_unsupported.dart'
if (dart.library.html) '_config_web.dart'
if (dart.library.io) '_config_io.dart' as impl;
abstract class Config {
/// Config file for the CacheManager.
/// [cacheKey] is used for the folder to store files and for the database
/// file name.
/// [stalePeriod] is the time duration in which a cache object is
/// considered 'stale'. When a file is cached but not being used for a
/// certain time the file will be deleted.
/// [maxNrOfCacheObjects] defines how large the cache is allowed to be. If
/// there are more files the files that haven't been used for the longest
/// time will be removed.
/// [repo] is the [CacheInfoRepository] which stores the cache metadata. On
/// Android, iOS and macOS this defaults to [CacheObjectProvider], a
/// sqflite implementation due to legacy. On web this defaults to
/// [NonStoringObjectProvider]. On the other platforms this defaults to
/// [JsonCacheInfoRepository].
/// The [fileSystem] defines where the cached files are stored and the
/// [fileService] defines where files are fetched, for example online.
factory Config(
String cacheKey, {
Duration stalePeriod,
int maxNrOfCacheObjects,
CacheInfoRepository repo,
FileSystem fileSystem,
FileService fileService,
}) = impl.Config;
String get cacheKey;
Duration get stalePeriod;
int get maxNrOfCacheObjects;
CacheInfoRepository get repo;
FileSystem get fileSystem;
FileService get fileService;
}
这个impl
也不知道具体是个什么东西,姑且认为是整个Flutter
运行环境吧,这样的话,随着图片的增长,这个缓存把整个Flutter
内存都占了也是可能的
指定缓存解决
既然默认的缓存有可能会占满整个
Flutter
的内存,那么指定缓存参数cacheManager,限制一下缓存大小就可以解决这个类似“崩溃”的问题。真的不用太着急。这篇文章[译]Flutter缓存管理库flutter_cache_manager 提到了指定缓存的方法。真的蛮简洁的,只是这单例的写法,也算是开了脑洞吧(静态类成员,确实只有一个)
class CustomCacheManager {
static const key = 'customCacheKey';
static CacheManager instance = CacheManager(
Config(
key,
stalePeriod: const Duration(days: 7),
maxNrOfCacheObjects: 20,
repo: JsonCacheInfoRepository(databaseName: key),
fileSystem: IOFileSystem(key),
fileService: HttpFileService(),
),
);
}
- 看了下代码,同事的解决方案应该是参考了上面那篇文章。
class NetworkImageWidget extends StatelessWidget {
/// ... ...
return CachedNetworkImage(
cacheManager: CustomCacheManager.instance,
imageUrl: actualUrl,
placeholder: (context, url) => _buildPlaceholderImage(actualPlaceholder),
errorWidget: (context, url, error) => _buildPlaceholderImage(actualPlaceholder),
width: width,
height: height,
);
}
class CustomCacheManager {
static const key = 'image_cache_key';
static CacheManager instance = CacheManager(
Config(
key,
stalePeriod: const Duration(days: 7),
maxNrOfCacheObjects: 100,
),
);
}
100这个数字虽然是拍脑袋给的,不过事后来看还真的有点道理。每张照片大约2M,缓存100张,就是200M,占总量2G的十分之一,也算合理。
关于CacheManager
既然
cached_network_image
使用flutter_cache_manager
作为缓存,那么我们指定缓存,也会用相同的。flutter_cache_manager
作为缓存,基本的读写功能是齐全的,参数配置也简洁,整体是很好的。我们的应用需要判断某张图片是否在缓存中,在界面上显示略有不同。但是
CacheManager
没有判断是否在缓存中的方法。下面这个读取文件信息的方法有点像,可以考虑利用一下:
/// Get the file from the cache.
/// Specify [ignoreMemCache] to force a re-read from the database
@override
Future<FileInfo?> getFileFromCache(String key,
{bool ignoreMemCache = false}) =>
_store.getFile(key, ignoreMemCache: ignoreMemCache);
///Returns the file from memory if it has already been fetched
@override
Future<FileInfo?> getFileFromMemory(String key) =>
_store.getFileFromMemory(key);
- 我们猜测这里的
key
应该是图片的url
,实际试了一下,确实是。
/// 判断图片是否存在缓存中
Future<bool> isPhotoInCache(String urlString) async {
bool isIn = false;
var fileInfo = await photoCache.getFileFromCache(urlString);
if (fileInfo != null) {
isIn = true;
debugPrint('图片已经在缓存中,url:${fileInfo.originalUrl}');
}
return isIn;
}