flutter_cache_manager本地缓存之BaseCacheManager、CacheManager源码解析(一)

2,039 阅读12分钟

flutter_cache_manager

本地缓存是常用的辅助功能。接下来,我们一起探究下经典的本地缓存三方库flutter_cache_manager

CacheManager 用于下载和缓存应用缓存目录中的文件。 缓存多长时间可通过各种设置来修改。 它使用 Cache-Control HTTP 头部来高效提取文件。

flutter_cache_manager | Flutter Package (flutter-io.cn)

BaseCacheManager抽象类

BaseCacheManagerflutter_cache_manager库的一个抽象类,定义了缓存管理器的基本结构。它提供了一些方法来检查、获取、清除缓存以及缓存文件的路径。在flutter_cache_manager中,缓存管理器是为了帮助开发人员在应用中管理和处理缓存的类。

getSingleFile

/// Get the file from the cache and/or online, depending on availability and age.
/// Downloaded form [url], [headers] can be used for example for authentication.
/// When a file is cached it is return directly, when it is too old the file is
/// downloaded in the background. When a cached file is not available the
/// newly downloaded file is returned.
Future<File> getSingleFile(
  String url, {
  String key,
  Map<String, String> headers,
});

getSingleFileBaseCacheManager中的一个方法,它接受一个URL以及可选的keyheaders参数,返回一个Future<File>对象。

该方法的主要功能是从缓存和/或在线获取文件,具体取决于文件的可用性和年龄。如果文件已被缓存,则直接返回缓存中的文件,否则会在后台下载文件。如果缓存文件不可用,则会返回新下载的文件。

如果指定了key,则使用它作为文件名进行缓存。如果未指定key,则使用URL的哈希值作为文件名。headers参数可以用于添加HTTP请求头,例如用于身份验证。

总体来说,getSingleFile方法提供了一个方便的方式来获取文件,同时最大限度地减少对网络资源的依赖。如果文件已被缓存,则从缓存获取文件可以加快加载速度,并减少对网络的请求。

CacheManager 可用多种方式获取一个文件。获取单个文件最简单的方式是调用 .getSingleFile 。

getFileStream

/// Get the file from the cache and/or online, depending on availability and age.
/// Downloaded form [url], [headers] can be used for example for authentication.
/// The files are returned as stream. First the cached file if available, when the
/// cached file is too old the newly downloaded file is returned afterwards.
///
/// The [FileResponse] is either a [FileInfo] object for fully downloaded files
/// or a [DownloadProgress] object for when a file is being downloaded.
/// The [DownloadProgress] objects are only dispatched when [withProgress] is
/// set on true and the file is not available in the cache. When the file is
/// returned from the cache there will be no progress given, although the file
/// might be outdated and a new file is being downloaded in the background.
Stream<FileResponse> getFileStream(String url,
    {String? key, Map<String, String>? headers, bool withProgress});

BaseCacheManager 类的 getFileStream 方法用于获取一个文件的流,这个文件可以从缓存或者网络获取。它接受以下参数:

  • url: 要获取的文件的 URL。
  • key: 用于唯一标识此文件的键。如果未提供,则会使用 URL 作为键。
  • headers: 要包含在下载请求中的标头。
  • withProgress: 是否在下载期间发布下载进度更新。如果设置为 true 并且文件不在缓存中,则会发出 DownloadProgress 对象,否则将不提供进度更新。

这个方法返回一个 Stream<FileResponse> 流,其中 FileResponse 是一个抽象类,它有两个实现类:

  • FileInfo: 完全下载的文件信息。
  • DownloadProgress: 正在下载的文件进度信息。

当文件被缓存时,它会直接返回,当文件过期时,它会在后台下载。当缓存中没有可用的文件时,将返回新下载的文件。

值得注意的是,如果要使用此方法,请确保您已经导入了 dart:async 库,因为它需要返回一个流。

downloadFile

///Download the file and add to cache
Future<FileInfo> downloadFile(String url,
    {String? key, Map<String, String>? authHeaders, bool force = false});

downloadFileBaseCacheManager的方法,用于下载文件并将其添加到缓存中。它的参数包括url表示要下载的文件的URL,key表示缓存中存储文件的键值,authHeaders用于身份验证的头部信息,force表示是否强制下载。该方法返回一个Future<FileInfo>对象,该对象表示下载完成的文件信息。

在下载文件时,downloadFile首先会尝试从缓存中查找文件。如果文件存在且未过期,则返回缓存文件。否则,它将下载文件并将其添加到缓存中。如果force参数设置为true,则无论缓存中是否存在文件,都将下载文件。

下载文件时,BaseCacheManager使用HttpClient类来发出HTTP请求。默认情况下,HttpClient会尝试重用现有的TCP连接以提高性能。下载的文件被保存在应用程序的缓存目录中。

getFileFromCache

/// Get the file from the cache.
/// Specify [ignoreMemCache] to force a re-read from the database
Future<FileInfo?> getFileFromCache(String key, {bool ignoreMemCache = false});

该方法用于从缓存中获取文件,可指定是否忽略内存缓存。它接受一个 key 参数,用于查找缓存中的文件。当 ignoreMemCache 参数设置为 false 时,方法首先尝试从内存缓存中获取文件。如果文件不在内存缓存中,它将尝试从磁盘缓存中获取文件。如果文件不在磁盘缓存中,则返回 null。

如果 ignoreMemCache 参数设置为 true,则该方法将直接从磁盘缓存中获取文件,而不检查内存缓存。

getFileFromMemory

///Returns the file from memory if it has already been fetched
Future<FileInfo?> getFileFromMemory(String key);

这个方法用于从内存中获取指定key对应的文件信息(FileInfo),如果该文件信息已经被获取并保存在内存中,可以直接返回;否则返回null

putFile


/// Put a file in the cache. It is recommended to specify the [eTag] and the
/// [maxAge]. When [maxAge] is passed and the eTag is not set the file will
/// always be downloaded again. The [fileExtension] should be without a dot,
/// for example "jpg". When cache info is available for the url that path
/// is re-used.
/// The returned [File] is saved on disk.
Future<File> putFile(
  String url,
  Uint8List fileBytes, {
  String? key,
  String? eTag,
  Duration maxAge = const Duration(days: 30),
  String fileExtension = 'file',
});

这个方法用于将文件保存到缓存中,其中包括文件的URL、文件内容、缓存的键(可选)、ETag(可选)、最大缓存时间(默认为30天)和文件扩展名(默认为'file')等参数。在保存文件时,如果缓存中已存在相同键的文件,则先删除旧文件。方法返回一个Future,返回保存的文件。

putFileStream

/// Put a byte stream in the cache. When using an existing file you can use
/// file.openRead(). It is recommended to specify  the [eTag] and the
/// [maxAge]. When [maxAge] is passed and the eTag is not set the file will
/// always be downloaded again. The [fileExtension] should be without a dot,
/// for example "jpg". When cache info is available for the url that path
/// is re-used.
/// The returned [File] is saved on disk.
Future<File> putFileStream(
  String url,
  Stream<List<int>> source, {
  String? key,
  String? eTag,
  Duration maxAge = const Duration(days: 30),
  String fileExtension = 'file',
});

putFileStream方法与putFile方法类似,不同之处在于它可以将字节流缓存到磁盘中。传入的参数source是一个Stream<List<int>>类型的字节流,可以是从文件中读取的字节流,也可以是通过网络下载的字节流。

putFile方法一样,也可以通过传入key参数来自定义缓存文件的路径,传入eTag参数来指定缓存文件的标识符,传入maxAge参数来指定缓存文件的最大存储时间。如果可用,则会重用与URL对应的缓存文件路径。最后,返回一个Future<File>对象,表示已保存在磁盘上的缓存文件。

removeFile

/// Remove a file from the cache
Future<void> removeFile(String key);

该方法用于从缓存中删除指定键的文件。

参数:

  • key:要删除的文件的键。

返回值:Future<void>

emptyCache

/// Removes all files from the cache
Future<void> emptyCache();

该方法用于清空缓存,将所有文件从缓存中删除。调用该方法将返回一个Future对象,该对象在清空缓存完成时将被解析。可以在emptyCache()方法后使用getFileFromCache()来检查文件是否被成功删除。

dispose

/// Closes the cache database
Future<void> dispose();

该方法用于关闭缓存数据库。当你使用BaseCacheManager类创建一个缓存管理器对象时,它会打开一个数据库连接以用于读取和写入缓存信息。当你完成缓存操作时,应该使用该方法来关闭数据库连接,释放资源并确保所有文件已保存在磁盘上。

例如,当你的Flutter应用程序关闭时,你可以在dispose方法中调用该方法以关闭缓存数据库连接。

CacheManager实现类

class CacheManager implements BaseCacheManager {
static CacheManagerLogLevel logLevel = CacheManagerLogLevel.none;
}

该类是BaseCacheManager的基本实现,应该作为单个实例使用。它提供了对缓存的基本管理和操作,包括获取、存储、删除和清空缓存等功能。另外,该类还实现了dispose()方法,用于关闭缓存数据库。

logLevel是一个静态变量,用于设置缓存管理器的日志输出级别。可以设置为CacheManagerLogLevel.noneCacheManagerLogLevel.errorsCacheManagerLogLevel.debugCacheManagerLogLevel.all。日志输出级别越高,输出的信息就越详细。

CacheManager(Config config)、CacheManager.custom(Config config, {CacheStore?cacheStore,WebHelper? webHelper,})

/// Creates a new instance of a cache manager. This can be used to retrieve
/// files from the cache or download them online. The http headers are used
/// for the maximum age of the files. The BaseCacheManager should only be
/// used in singleton patterns.
///
/// The [_cacheKey] is used for the sqlite database file and should be unique.
/// Files are removed when they haven't been used for longer than [stalePeriod]
/// or when this cache has grown too big. When the cache is larger than [maxNrOfCacheObjects]
/// files the files that haven't been used longest will be removed.
/// The [fileService] can be used to customize how files are downloaded. For example
/// to edit the urls, add headers or use a proxy. You can also choose to supply
/// a CacheStore or WebHelper directly if you want more customization.
CacheManager(Config config)
    : _config = config,
      _store = CacheStore(config) {
  _webHelper = WebHelper(_store, config.fileService);
}

@visibleForTesting
CacheManager.custom(
  Config config, {
  CacheStore? cacheStore,
  WebHelper? webHelper,
})  : _config = config,
      _store = cacheStore ?? CacheStore(config) {
  _webHelper = webHelper ?? WebHelper(_store, config.fileService);
}

创建一个新的 CacheManager 实例,它可以用于从缓存中检索文件或在线下载文件。

http headers 用于设置文件的最大存储时间。BaseCacheManager 应该只在单例模式下使用。

[_cacheKey] 用于 sqlite 数据库文件,应该是唯一的。当文件未使用的时间超过 [stalePeriod] 或者缓存文件数量超过 [maxNrOfCacheObjects] 时,文件将被删除。

如果缓存超过 [maxNrOfCacheObjects],则将删除最久未使用的文件。

[fileService] 可用于自定义文件下载方式。例如,编辑 URL、添加 headers 或使用代理。

如果需要更多的自定义选项,可以选择直接提供 CacheStore 或 WebHelper。

辅助类

final Config _config;

/// Store helper for cached files
final CacheStore _store;

/// Get the underlying store helper
CacheStore get store => _store;

/// WebHelper to download and store files
late final WebHelper _webHelper;

/// Get the underlying web helper
WebHelper get webHelper => _webHelper;

getSingleFile

/// Get the file from the cache and/or online, depending on availability and age.
/// Downloaded form [url], [headers] can be used for example for authentication.
/// When a file is cached and up to date it is return directly, when the cached
/// file is too old the file is downloaded and returned after download.
/// When a cached file is not available the newly downloaded file is returned.
@override
Future<File> getSingleFile(
  String url, {
  String? key,
  Map<String, String>? headers,
}) async {
  key ??= url;
  final cacheFile = await getFileFromCache(key);
  if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
    return cacheFile.file;
  }
  return (await downloadFile(url, key: key, authHeaders: headers)).file;
}

getFileStream

/// Get the file from the cache and/or online, depending on availability and age.
/// Downloaded form [url], [headers] can be used for example for authentication.
/// The files are returned as stream. First the cached file if available, when the
/// cached file is too old the newly downloaded file is returned afterwards.
///
/// The [FileResponse] is either a [FileInfo] object for fully downloaded files
/// or a [DownloadProgress] object for when a file is being downloaded.
/// The [DownloadProgress] objects are only dispatched when [withProgress] is
/// set on true and the file is not available in the cache. When the file is
/// returned from the cache there will be no progress given, although the file
/// might be outdated and a new file is being downloaded in the background.
@override
Stream<FileResponse> getFileStream(String url,
    {String? key, Map<String, String>? headers, bool withProgress = false}) {
  key ??= url;
  final streamController = StreamController<FileResponse>();
  _pushFileToStream(streamController, url, key, headers, withProgress);
  return streamController.stream;
}
Future<void> _pushFileToStream(StreamController streamController, String url,
    String? key, Map<String, String>? headers, bool withProgress) async {
  key ??= url;
  FileInfo? cacheFile;
  try {
    cacheFile = await getFileFromCache(key);
    if (cacheFile != null) {
      streamController.add(cacheFile);
      withProgress = false;
    }
  } catch (e) {
    cacheLogger.log(
        'CacheManager: Failed to load cached file for $url with error:\n$e',
        CacheManagerLogLevel.debug);
  }
  if (cacheFile == null || cacheFile.validTill.isBefore(DateTime.now())) {
    try {
      await for (var response
          in _webHelper.downloadFile(url, key: key, authHeaders: headers)) {
        if (response is DownloadProgress && withProgress) {
          streamController.add(response);
        }
        if (response is FileInfo) {
          streamController.add(response);
        }
      }
    } catch (e) {
      cacheLogger.log(
          'CacheManager: Failed to download file from $url with error:\n$e',
          CacheManagerLogLevel.debug);
      if (cacheFile == null && streamController.hasListener) {
        streamController.addError(e);
      }
    }
  }
  unawaited(streamController.close());
}

该方法是私有方法,用于将文件推送到流中以便处理。它接收一个StreamController,它将在下载过程中输出FileInfo和DownloadProgress对象。如果可用,它首先从缓存中获取文件,如果缓存文件未过期,则直接将其推送到流中。如果缓存文件不可用或已过期,则使用WebHelper下载文件,并将其推送到流中。如果下载失败,它会将错误推送到流中,以便处理它。最后,它关闭了流。这是getSingleFile和getFileStream中的一个私有方法。

downloadFile

///Download the file and add to cache
@override
Future<FileInfo> downloadFile(String url,
    {String? key,
    Map<String, String>? authHeaders,
    bool force = false}) async {
  key ??= url;
  var fileResponse = await _webHelper
      .downloadFile(
        url,
        key: key,
        authHeaders: authHeaders,
        ignoreMemCache: force,
      )
      .firstWhere((r) => r is FileInfo);
  return fileResponse as FileInfo;
}

getFileFromCache

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

getFileFromMemory

///Returns the file from memory if it has already been fetched
@override
Future<FileInfo?> getFileFromMemory(String key) =>
    _store.getFileFromMemory(key);

putFile

/// Put a file in the cache. It is recommended to specify the [eTag] and the
/// [maxAge]. When [maxAge] is passed and the eTag is not set the file will
/// always be downloaded again. The [fileExtension] should be without a dot,
/// for example "jpg". When cache info is available for the url that path
/// is re-used.
/// The returned [File] is saved on disk.
@override
Future<File> putFile(
  String url,
  Uint8List fileBytes, {
  String? key,
  String? eTag,
  Duration maxAge = const Duration(days: 30),
  String fileExtension = 'file',
}) async {
  key ??= url;
  var cacheObject = await _store.retrieveCacheData(key);
  cacheObject ??= CacheObject(
    url,
    key: key,
    relativePath: '${const Uuid().v1()}.$fileExtension',
    validTill: DateTime.now().add(maxAge),
  );

  cacheObject = cacheObject.copyWith(
    validTill: DateTime.now().add(maxAge),
    eTag: eTag,
  );

  final file = await _config.fileSystem.createFile(cacheObject.relativePath);
  await file.writeAsBytes(fileBytes);
  unawaited(_store.putFile(cacheObject));
  return file;
}

这个方法用于将文件保存到缓存中,其中包括文件的URL、文件内容、缓存的键(可选)、ETag(可选)、最大缓存时间(默认为30天)和文件扩展名(默认为'file')等参数。在保存文件时,如果缓存中已存在相同键的文件,则先删除旧文件。方法返回一个Future,返回保存的文件。

putFileStream

/// Put a byte stream in the cache. When using an existing file you can use
/// file.openRead(). It is recommended to specify  the [eTag] and the
/// [maxAge]. When [maxAge] is passed and the eTag is not set the file will
/// always be downloaded again. The [fileExtension] should be without a dot,
/// for example "jpg". When cache info is available for the url that path
/// is re-used.
/// The returned [File] is saved on disk.
@override
Future<File> putFileStream(
  String url,
  Stream<List<int>> source, {
  String? key,
  String? eTag,
  Duration maxAge = const Duration(days: 30),
  String fileExtension = 'file',
}) async {
  key ??= url;
  var cacheObject = await _store.retrieveCacheData(key);
  cacheObject ??= CacheObject(url,
      key: key,
      relativePath: '${const Uuid().v1()}'
          '.$fileExtension',
      validTill: DateTime.now().add(maxAge));

  cacheObject = cacheObject.copyWith(
    validTill: DateTime.now().add(maxAge),
    eTag: eTag,
  );

  var file = await _config.fileSystem.createFile(cacheObject.relativePath);

  // Always copy file
  var sink = file.openWrite();
  await source
      // this map is need to map UInt8List to List<int>
      .map((event) => event)
      .pipe(sink);

  unawaited(_store.putFile(cacheObject));
  return file;
}

removeFile

/// Remove a file from the cache
@override
Future<void> removeFile(String key) async {
  final cacheObject = await _store.retrieveCacheData(key);
  if (cacheObject?.id != null) {
    await _store.removeCachedFile(cacheObject!);
  }
}

emptyCache

/// Removes all files from the cache
@override
Future<void> emptyCache() => _store.emptyCache();

dispose

/// Closes the cache database
@override
Future<void> dispose() async {
  await _config.repo.close();
}