cached_network_image實踐 2023-06-20 周二

簡介

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

企業(yè)微信截圖_369431f4-e23f-43ee-b7f8-38cba375e753.png

緩存

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

企業(yè)微信截圖_38551c09-a849-4f72-952b-6d05fcdf016f.png

關(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,然后errorWidgetplaceholder設(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;
  }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容