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;
  }
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容