cached_network_image实践 2023-06-20 周二

简介

Flutter提供的图片展示工具没有缓存功能,这个插件填补了空白。在Pub上,这个插件的评价也是很高的。

企业微信截图_369431f4-e23f-43ee-b7f8-38cba375e753.png

缓存

这个插件的缓存,调用了另外一个插件flutter_cache_manager

企业微信截图_38551c09-a849-4f72-952b-6d05fcdf016f.png

关于这一点,在cached_network_image的介绍中有提到,不过基本上很少有人注意。
大多数时候,感觉缓存应该是有的,访问过的大图,再次访问速度会比较快。只是平时用的时候根本不会去关心缓存的事情。基本上会按照官网介绍的用:

CachedNetworkImage(
        imageUrl: "http://via.placeholder.com/350x150",
        placeholder: (context, url) => CircularProgressIndicator(),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),

大部分时候,就是把placeholder换成一个本地图片,至于缓存,基本上不会在意。

由缓存导致的问题

  • 我们的是电商应用,所以图片较多,对CachedNetworkImage封装了之后,大量应用

  • 封装也是简单套了一层,提供了一张本地的静态图片作为placeholder,然后errorWidgetplaceholder设置成了一样

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;
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容