flutter_cache_manager本地缓存之DefaultCacheManager、ImageCacheManager源码解析(五)

2,495 阅读8分钟

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));
}

DefaultCacheManager 是继承自 CacheManagerImageCacheManager 的默认缓存管理器实现,旨在提供一个方便的缓存解决方案。具体来说,它是一个单例对象,使用 Config(key) 配置来初始化 CacheManager,并添加了图片缓存的能力。

CacheManager 是一个抽象类,它定义了一组管理缓存的 API,例如 getFileFromCacheputFileremoveFile 等方法。通过继承该类并实现其中的方法,可以创建一个自定义的缓存管理器。

ImageCacheManager 是一个 mixin,它提供了一些特定于图片缓存的方法,例如 getImageFileputImageFileremoveImageFile 等。通过继承该类和实现其方法,可以创建一个支持图片缓存的自定义缓存管理器。

DefaultCacheManager 的实现中,它继承了 CacheManagerImageCacheManager,并使用 Config(key) 来初始化 CacheManager,其中 key 是该缓存管理器的名称。通过这种方式,它可以使用 CacheManager 的所有方法,同时还添加了对图片缓存的支持。

此外,由于它是一个单例对象,因此在整个应用程序生命周期中只存在一个 DefaultCacheManager 实例,可以确保所有的文件和图片缓存操作都是针对同一实例进行的。

ImageCacheManager

const supportedFileNames = ['jpg', 'jpeg', 'png', 'tga', 'cur', 'ico'];
mixin ImageCacheManager on BaseCacheManager {}

ImageCacheManager 是一个 Dart mixin(混入)类,通过 mixin 的方式混合到 DefaultCacheManager 中。

getImageFile

final Map<String, Stream<FileResponse>> _runningResizes = {};
/// Returns a resized image file to fit within maxHeight and maxWidth. It
/// tries to keep the aspect ratio. It stores the resized image by adding
/// the size to the key or url. For example when resizing
/// https://via.placeholder.com/150 to max width 100 and height 75 it will
/// store it with cacheKey resized_w100_h75_https://via.placeholder.com/150.
///
/// When the resized file is not found in the cache the original is fetched
/// from the cache or online and stored in the cache. Then it is resized
/// and returned to the caller.
Stream<FileResponse> getImageFile(
  String url, {
  String? key,
  Map<String, String>? headers,
  bool withProgress = false,
  int? maxHeight,
  int? maxWidth,
}) async* {
  if (maxHeight == null && maxWidth == null) {
    yield* getFileStream(url,
        key: key, headers: headers, withProgress: withProgress);
    return;
  }
  key ??= url;
  var resizedKey = 'resized';
  if (maxWidth != null) resizedKey += '_w$maxWidth';
  if (maxHeight != null) resizedKey += '_h$maxHeight';
  resizedKey += '_$key';

  var fromCache = await getFileFromCache(resizedKey);
  if (fromCache != null) {
    yield fromCache;
    if (fromCache.validTill.isAfter(DateTime.now())) {
      return;
    }
    withProgress = false;
  }
  var runningResize = _runningResizes[resizedKey];
  if (runningResize == null) {
    runningResize = _fetchedResizedFile(
      url,
      key,
      resizedKey,
      headers,
      withProgress,
      maxWidth: maxWidth,
      maxHeight: maxHeight,
    ).asBroadcastStream();
    _runningResizes[resizedKey] = runningResize;
  }
  yield* runningResize;
  _runningResizes.remove(resizedKey);
}

该方法用于获取图片文件并可以按指定的最大高度和宽度进行缩放,保持原始图片的长宽比例。如果缓存中已经存在所需尺寸的缩放图片,则直接返回缓存中的图片,否则从缓存或者网络中获取原始图片,进行缩放后返回给调用者。方法参数包括图片的 URL,缓存键值,HTTP 请求头,是否需要进度报告,最大高度和最大宽度。如果不指定最大高度和宽度,则直接获取原始图片文件。如果指定了最大高度和宽度,则按照最大高度和宽度对图片进行缩放。对于缩放后的图片,缓存键值会自动加上resized前缀和指定的尺寸后缀,例如resized_w100_h75_https://via.placeholder.com/150,以避免缓存键值冲突。

该方法返回一个 Stream<FileResponse>,可以通过监听这个 Stream 来获得异步的文件响应结果。在获取响应结果前,该方法会先检查缓存是否已经存在所需的图片,如果存在,则直接返回缓存中的图片,并且如果缓存中的图片没有过期,则不会进行后续操作。如果缓存中不存在所需尺寸的图片,则会从网络或者缓存中获取原始图片,并且在获取原始图片后进行缩放。

如果需要缩放,就通过 _fetchedResizedFile 方法开始异步处理图片文件的下载和缩放,并将返回的 Stream 对象保存到 _runningResizes 字典中,以便后续可以直接使用。

如果之前已经在下载和缩放该图片文件,则直接使用之前保存的 _runningResizes 中的 Stream 对象进行后续操作,避免了重复下载和缩放同一张图片。当图片下载和缩放完成之后,将对应的 _runningResizes 中的 Stream 对象从字典中移除。

_fetchedResizedFile

Stream<FileResponse> _fetchedResizedFile(
  String url,
  String originalKey,
  String resizedKey,
  Map<String, String>? headers,
  bool withProgress, {
  int? maxWidth,
  int? maxHeight,
}) async* {
  await for (var response in getFileStream(
    url,
    key: originalKey,
    headers: headers,
    withProgress: withProgress,
  )) {
    if (response is DownloadProgress) {
      yield response;
    }
    if (response is FileInfo) {
      yield await _resizeImageFile(
        response,
        resizedKey,
        maxWidth,
        maxHeight,
      );
    }
  }
}

这是一个私有方法 _fetchedResizedFile,用于实际获取和调整大小图片文件的操作。它接受原始图片的URL、缓存的原始文件键、调整大小后缓存文件的键、可选的头信息、一个布尔值,表示是否需要包含下载进度,以及调整大小的最大宽度和最大高度。

在方法中,它使用 getFileStream 从URL获取文件流,并使用 yield* 返回其元素。如果元素是 DownloadProgress,则直接返回它以更新下载进度。如果元素是 FileInfo,则使用 _resizeImageFile 调整大小,然后返回调整大小后的文件信息。

_resizeImageFile

Future<FileInfo> _resizeImageFile(
  FileInfo originalFile,
  String key,
  int? maxWidth,
  int? maxHeight,
) async {
  var originalFileName = originalFile.file.path;
  var fileExtension = originalFileName.split('.').last;
  if (!supportedFileNames.contains(fileExtension)) {
    return originalFile;
  }

  var image = await _decodeImage(originalFile.file);

  var shouldResize = maxWidth != null
      ? image.width > maxWidth
      : false || maxHeight != null
          ? image.height > maxHeight
          : false;
  if (!shouldResize) return originalFile;
  if (maxWidth != null && maxHeight != null) {
    var resizeFactorWidth = image.width / maxWidth;
    var resizeFactorHeight = image.height / maxHeight;
    var resizeFactor = max(resizeFactorHeight, resizeFactorWidth);

    maxWidth = (image.width / resizeFactor).round();
    maxHeight = (image.height / resizeFactor).round();
  }

  var resized = await _decodeImage(originalFile.file,
      width: maxWidth, height: maxHeight, allowUpscaling: false);
  var resizedFile =
      (await resized.toByteData(format: ui.ImageByteFormat.png))!
          .buffer
          .asUint8List();
  var maxAge = originalFile.validTill.difference(DateTime.now());

  var file = await putFile(
    originalFile.originalUrl,
    resizedFile,
    key: key,
    maxAge: maxAge,
    fileExtension: fileExtension,
  );

  return FileInfo(
    file,
    originalFile.source,
    originalFile.validTill,
    originalFile.originalUrl,
  );
}

该方法用于对原始图片进行缩放操作,并将缩放后的图片保存在缓存中。

具体来说,该方法接受四个参数:

  • originalFile: 原始文件信息,包含了原始图片的File对象、缓存的来源以及过期时间等信息。
  • key: 缓存的键,即存储缩放后图片的文件名。
  • maxWidth: 缩放后图片的最大宽度。
  • maxHeight: 缩放后图片的最大高度。

方法内部首先会根据原始文件的路径获取文件扩展名,如果扩展名不在支持的类型列表内,则直接返回原始文件信息。

接着,方法会使用decodeImage方法将原始图片解码成一个Image对象,并检查是否需要对其进行缩放操作。如果不需要缩放,则直接返回原始文件信息。

如果需要缩放,则根据maxWidth和maxHeight计算缩放比例,并使用decodeImage方法对原始图片进行缩放。缩放完成后,将缩放后的图片保存为png格式的字节数组,并调用putFile方法将其存储到缓存中。

最后,方法返回一个FileInfo对象,其中包含了缩放后的图片File对象、缓存的来源、过期时间以及原始图片的URL。

_decodeImage

Future<ui.Image> _decodeImage(File file,
    {int? width, int? height, bool allowUpscaling = false}) {
  var shouldResize = width != null || height != null;
  var fileImage = FileImage(file);
  final image = shouldResize
      ? ResizeImage(fileImage,
          width: width, height: height, allowUpscaling: allowUpscaling)
      : fileImage as ImageProvider;
  final completer = Completer<ui.Image>();
  image
      .resolve(const ImageConfiguration())
      .addListener(ImageStreamListener((info, _) {
    completer.complete(info.image);
    image.evict();
  }));
  return completer.future;
}

该方法用于将文件解码成UI图像对象。它接受一个文件对象,以及可选的宽度和高度参数。如果提供了宽度或高度,则会尝试调整图像大小以适应给定的宽度和/或高度,并且allowUpscaling参数可以控制是否允许图像放大。如果没有提供宽度或高度参数,则不会调整图像大小。该方法返回一个Future,该Future最终将返回UI图像对象。

解释说明

maxWidth 、maxHeight

maxWidthmaxHeight是指调整后的图片最大宽度和高度。在对图片进行调整大小的时候,该方法会检查图片的实际宽度和高度是否超过了给定的maxWidthmaxHeight,如果超过了就进行缩小操作,如果没有超过就不做任何操作,返回原图。如果同时给定了maxWidthmaxHeight,那么调整后的图片宽度和高度将按照宽高比例缩小,但是最终的宽度和高度都不会超过给定的maxWidthmaxHeight

缓存到内存、磁盘的图像,是缩放后的图像。

精妙之处

flutter_cache_manager 是一个 Flutter 缓存管理器,它允许 Flutter 应用在网络请求和本地缓存之间进行平滑的切换,同时提供了一些有用的特性和 API,设计的精妙之处主要有以下几点:

  1. 多种缓存策略可选flutter_cache_manager 提供了多种缓存策略,包括内存缓存、文件系统缓存、自定义缓存等,用户可以根据自己的需求选择合适的缓存策略。
  2. 自动缓存文件flutter_cache_manager 会自动缓存网络请求的文件,并根据 HTTP 响应头中的缓存控制信息(如 Cache-Control、ETag 等)来决定文件是否需要重新请求和缓存,从而避免了重复的网络请求。
  3. 异步处理flutter_cache_manager 中的大部分 API 都是异步的,这意味着它们不会阻塞主线程,可以提升应用的性能和流畅度。
  4. 简单易用flutter_cache_manager 的 API 设计简单明了,易于上手,同时还提供了详细的文档和示例,方便开发者使用。

综上所述,flutter_cache_manager 是一个功能强大、设计精妙、易于使用的 Flutter 缓存管理库,它可以帮助开发者更好地管理应用中的缓存,并提升应用的性能和用户体验。