簡介
Flutter提供的圖片展示工具沒有緩存功能,這個插件填補了空白。在Pub上,這個插件的評價也是很高的。

緩存
這個插件的緩存,調(diào)用了另外一個插件flutter_cache_manager

關(guān)于這一點,在cached_network_image的介紹中有提到,不過基本上很少有人注意。
大多數(shù)時候,感覺緩存應(yīng)該是有的,訪問過的大圖,再次訪問速度會比較快。只是平時用的時候根本不會去關(guān)心緩存的事情?;旧蠒凑展倬W(wǎng)介紹的用:
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
大部分時候,就是把placeholder換成一個本地圖片,至于緩存,基本上不會在意。
由緩存導致的問題
我們的是電商應(yīng)用,所以圖片較多,對
CachedNetworkImage封裝了之后,大量應(yīng)用封裝也是簡單套了一層,提供了一張本地的靜態(tài)圖片作為
placeholder,然后errorWidget和placeholder設(shè)置成了一樣
class NetworkImageWidget extends StatelessWidget {
/// ... ...
return CachedNetworkImage(
imageUrl: actualUrl,
placeholder: (context, url) => _buildPlaceholderImage(actualPlaceholder),
errorWidget: (context, url, error) => _buildPlaceholderImage(actualPlaceholder),
width: width,
height: height,
);
}
幾乎所有的圖片都調(diào)用這個封裝之后的控件,使用下來,體驗還是很不錯的。
我們的應(yīng)用,提供商品的照片,為了照片質(zhì)量,都是用
iPhone12拍攝的,采用的是默認的4032*3024分辨率,上傳到阿里cnd之后,一張照片的大小有2M左右;后來,用戶反饋說
APP會崩潰,特別是在查看商品的高清照片時更容易發(fā)生。連上手機折騰,最后發(fā)現(xiàn),是
CachedNetworkImage占用內(nèi)存太大(印象中是2G的樣子),導致APP退出,現(xiàn)象和崩潰差不多……
怎么會這樣?怎么解決?在線等,挺急的……
默認緩存背鍋
從效果來看,肯定是有緩存的,
我們封裝的組件并沒有指定緩存,那么組件內(nèi)部使用了默認的緩存
CachedNetworkImage也是一個殼,內(nèi)部真正起作用的是CachedNetworkImageProvider;官網(wǎng)文檔也提到用這兩個作用是一樣的,確實是這樣。
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);
緩存參數(shù)
cacheManager,也被傳給了CachedNetworkImageProvider
- 繼續(xù)往下走,看
CachedNetworkImageProvider做了什么。從下面這段代碼可以看出,緩存參數(shù)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),
);
}
- 到這里,問題算搞清楚了一半:確實有一個默認的緩存,所以平時不需要指定緩存。那么反過來,內(nèi)存占用過大基本上也是這個默認緩存
DefaultCacheManager()造成的。繼續(xù)往下看:
/// 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)結(jié)構(gòu)就是配置緩存參數(shù)的地方
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運行環(huán)境吧,這樣的話,隨著圖片的增長,這個緩存把整個Flutter內(nèi)存都占了也是可能的
指定緩存解決
既然默認的緩存有可能會占滿整個
Flutter的內(nèi)存,那么指定緩存參數(shù)cacheManager,限制一下緩存大小就可以解決這個類似“崩潰”的問題。真的不用太著急。這篇文章[譯]Flutter緩存管理庫flutter_cache_manager 提到了指定緩存的方法。真的蠻簡潔的,只是這單例的寫法,也算是開了腦洞吧(靜態(tài)類成員,確實只有一個)
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(),
),
);
}
- 看了下代碼,同事的解決方案應(yīng)該是參考了上面那篇文章。
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這個數(shù)字雖然是拍腦袋給的,不過事后來看還真的有點道理。每張照片大約2M,緩存100張,就是200M,占總量2G的十分之一,也算合理。
關(guān)于CacheManager
既然
cached_network_image使用flutter_cache_manager作為緩存,那么我們指定緩存,也會用相同的。flutter_cache_manager作為緩存,基本的讀寫功能是齊全的,參數(shù)配置也簡潔,整體是很好的。我們的應(yīng)用需要判斷某張圖片是否在緩存中,在界面上顯示略有不同。但是
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應(yīng)該是圖片的url,實際試了一下,確實是。
/// 判斷圖片是否存在緩存中
Future<bool> isPhotoInCache(String urlString) async {
bool isIn = false;
var fileInfo = await photoCache.getFileFromCache(urlString);
if (fileInfo != null) {
isIn = true;
debugPrint('圖片已經(jīng)在緩存中,url:${fileInfo.originalUrl}');
}
return isIn;
}