likes
comments
collection
share

【Flutter】更进一步,文件缓存管理的MaxSize,LRU,加密等功能实现

作者站长头像
站长
· 阅读数 2

Flutter文件缓存管理的一些特定功能的实现

前言

前文中简单的文件缓存,我们可以存入对象数据,也就是 Map 的 Json 数据,因为 Flutter 的 Entity 对象都是自带 formJson 与 toJson 的,我们就可以简单的理解 Json 就是我们的对象。

我们还能存入基本数据类型,并且定义的移除 key 的逻辑,和全部删除的功能。我们还简单的设置了最大缓存容量,以及过期的删除。

我们在之前的文章结尾留下了几个优化的点,最大容量管理,最近最少使用的管理,以及简单的加密解密功能,本文就一起看看更进一步如何优化缓存的写入。

一、最大容量管理

前文的最大容量管理,我们在存入缓存的时候,每次存入都会遍历两次,一次需要获取到最新的当前容量,以及最新的过期文件去删除。

有两个问题,一个是不需要每次都遍历获取最新的容量,其次如果文件都没有过期,则只能删除最早的存入时间。

那我们可以在初始化的时候赋值,然后自己手动的管理变量:

  int cacheSize = 0; //当前的总缓存大小
  int cacheCount = 0; //当前的总缓存文件数量

// 初始化 - 获取到缓存路径
  Future _init() async {
    cachePath = DirectoryUtil.getTempPath(category: _fileCategory);
    if (cachePath != null) {
      //尝试异步创建自定义的文件夹
      await DirectoryUtil.createTempDir(category: _fileCategory);

      //计算当前文件夹
      Directory cacheDir = Directory(cachePath!);
      List<File> cacheFiles = cacheDir.listSync().whereType<File>().toList();

      if (cacheFiles.isNotEmpty) {
        // 计算缓存文件夹的总大小
        for (File file in cacheFiles) {
          cacheSize += await file.length();
          cacheCount += 1;
        }
      }
    } else {
      throw Exception('DirectoryUtil 无法获取到Cache文件夹,可能 DirectoryUtil 没有初始化,请检查!');
    }
  }

存入的时候我们就能统一的管理

 Future<void> checkAndWriteFile(File file, String jsonString) async {
    Directory cacheDir = Directory(cachePath!);
    List<File> cacheFiles = cacheDir.listSync().whereType<File>().toList();

    if (cacheCount != 0) {
      //如果总大小超过限制,依次删除文件直到满足条件
      while (maxSizeInBytes > 0 && cacheSize > maxSizeInBytes) {
        File? fileToDelete;
        int oldestTimestamp = 0;

        for (File file in cacheFiles) {
          final key = path.basename(file.path);
          //取出全部的 Json 文本与对象
          final jsonString = await file.readAsString();
          final jsonData = jsonDecode(jsonString) as Map<String, dynamic>?;
          if (jsonData == null) {
            continue;
          }

          //取出对应 key 的 Json 对象
          final jsonEntry = jsonData[key] as Map<String, dynamic>?;
          if (jsonEntry == null || _isExpired(jsonEntry)) {
            fileToDelete = file;
            break;
          } else {
            final timestamp = jsonData['timestamp'] as int?;
            if (timestamp != null) {
              if (timestamp < oldestTimestamp) {
                fileToDelete = file;
                oldestTimestamp = timestamp;
              }
            }
          }
        }

        //遍历文件结束之后需要删除处理的逻辑
        if (fileToDelete != null) {
          await fileToDelete.delete();
          // 更新缓存文件列表
          cacheFiles.remove(fileToDelete);
         await decreaseSize(fileToDelete);
        } else {
          break;
        }

      }

      //最后写入文件
      await file.writeAsString(jsonString);
    } else {
      // 如果是空文件夹,直接写即可
      await file.writeAsString(jsonString);
    }
  }

    // 减少容量与数量
  Future<void> decreaseSize(File file) async {
    cacheSize = cacheSize - await _calculateSize(file);
    cacheCount = cacheCount - 1; //当前数量减1
  }

  // 增加容量与数量
  Future<void> increaseSize(File file) async {
    cacheSize += await _calculateSize(file);
    cacheCount += 1; //当前数量加1
  }

   /// 移除指定的 Key 对应的缓存文件
  Future<void> removeByKey(String key) async {
    if (cachePath == null) {
      await _init();
    }

    String path = '$cachePath/$key';
    final file = File(path);
    if (await file.exists()) {
      await file.delete();
      await decreaseSize(file);
    }
  }

我们在存入的时候统一管理,并且自己控制添加与删除的容量与数量。这样就能少一次全量的遍历获取最新的缓存容量。

二、最近最少使用的管理

当我们没有过期的缓存,还是超出了最大容量会怎么样?

看我们之前的代码是删除掉存入时间最早的文件,其实不合理的。我们应该是优先删除过期的文件,如果没有过期的文件则删除最近最少使用的文件。

如何获取操作文件的时间戳,我们可以用 File 的 lastModified 属性获取最后操作时间。

那么我们修改的代码如下:

  int cacheSize = 0; //当前的总缓存大小
  int cacheCount = 0; //当前的总缓存文件数量
  Map<File, int> lastUsageDates = {}; //最近使用的记录

   // 初始化 - 获取到缓存路径
  Future _init() async {
    cachePath = DirectoryUtil.getTempPath(category: _fileCategory);
    if (cachePath != null) {
      //尝试异步创建自定义的文件夹
      await DirectoryUtil.createTempDir(category: _fileCategory);

      //计算当前文件夹
      Directory cacheDir = Directory(cachePath!);
      List<File> cacheFiles = cacheDir.listSync().whereType<File>().toList();

      if (cacheFiles.isNotEmpty) {
        // 计算缓存文件夹的总大小
        for (File file in cacheFiles) {
          cacheSize += await file.length();
          cacheCount += 1;
          lastUsageDates[file] = (await file.lastModified()).millisecondsSinceEpoch; //最近使用的毫秒值
        }
      }
    } else {
      throw Exception('DirectoryUtil 无法获取到Cache文件夹,可能 DirectoryUtil 没有初始化,请检查!');
    }
  }

存入缓存的代码如下:

 /*
     核心写入方法
     检查是否超过最大限制,并写入文件
   */
  Future<void> checkAndWriteFile(File file, String jsonString) async {
    Log.d("cacheSize:$cacheSize cacheCount:$cacheCount");

    if (cacheCount != 0) {
      //如果总大小超过限制,依次删除文件直到满足条件
      while (cacheSize > 0 && cacheSize > maxSizeInBytes) {
        Log.d("FileCacheManager 超过了最大限制");
        File? fileToDelete; //过期文件的删除

        int? oldestUsage; //最久未使用的文件时间戳
        File? mostLongUsedFile; //最近未使用的文件

        for (var entry in lastUsageDates.entries) {
          //过期文件的处理
          final key = path.basename(file.path);
          //取出全部的 Json 文本与对象
          final jsonString = await entry.key.readAsString();
          final jsonData = jsonDecode(jsonString) as Map<String, dynamic>?;
          if (jsonData == null) {
            //说明当前存入的缓存不是一个Json格式
            continue;
          }

          //取出对应 key 的 Json 对象
          final jsonEntry = jsonData[key] as Map<String, dynamic>?;
          if (jsonEntry == null || _isExpired(jsonEntry)) {
            fileToDelete = file;
            break;
          } else {
            fileToDelete = null;
          }

          //最近未使用的逻辑
          if (mostLongUsedFile == null) {
            mostLongUsedFile = entry.key;
            oldestUsage = entry.value;
          } else {
            int lastValueUsage = entry.value;
            if (lastValueUsage < oldestUsage!) {
              oldestUsage = lastValueUsage;
              mostLongUsedFile = entry.key;
            }
          }
        }

        Log.d("FileCacheManager fileToDelete:$fileToDelete mostLongUsedFile:$mostLongUsedFile");

        if (fileToDelete != null) {
          await fileToDelete.delete();
          await decreaseSize(fileToDelete);

        } else if (mostLongUsedFile != null) {
          await mostLongUsedFile.delete();
          await decreaseSize(mostLongUsedFile);

        } else {
          break;
        }

      }

      Log.d("FileCacheManager 最后写入的文件:$file");
      //最后写入文件
      await file.writeAsString(jsonString);
      await increaseSize(file);

    } else {

      // 如果是空文件夹,直接写即可
      await file.writeAsString(jsonString);
      await increaseSize(file);
    }
  }

   // 减少容量与数量
  Future<void> decreaseSize(File file) async {
    cacheSize = cacheSize - await _calculateSize(file);
    cacheCount = cacheCount - 1; //当前数量减1
    if (lastUsageDates.containsKey(file)) {
      lastUsageDates.remove(file);
    }
  }

  // 增加容量与数量
  Future<void> increaseSize(File file) async {
    cacheSize += await _calculateSize(file);
    cacheCount += 1; //当前数量加1
    lastUsageDates[file] = (await file.lastModified()).millisecondsSinceEpoch;
  }

  // 根据文件路径计算文件大小
  Future<int> _calculateSize(File file) async {
    if (await file.exists()) {
      return await file.length();
    }
    return 0;
  }

这样就更合理一点,避免了常用的缓存文件每次都被无脑删除导致体验不好。

三、加密解密的功能

本来我们的缓存就是存在内置沙盒中,已经是很安全了,如果非想要做加密也可以对文件名与文件内容分别做加密。

文件名我们只需要比对是否相同可以用MD5的方式,而文本内容的加密,我们可以选择Base64或者自定义的RSA加密都可以。

这里我以 MD5 与 Base64 简单加密为例子。

如果我们的 key 需要 MD5 方式的加密,那么我们在存入的方法中就能很方便的完成

Future<void> putJsonByKey(String key, Map<String, dynamic> jsonData, {Duration? expiration}) async {
    // 加锁
    await _lock.synchronized(() async {
      if (cachePath == null) {
        await _init();
      } else {
        Directory cacheDir = Directory(cachePath ?? '');
        if (!await cacheDir.exists()) {
          await DirectoryUtil.createTempDir(category: _fileCategory);
        }
      }

      //加密Key
      key = EncryptUtil.encodeMd5(key);

      final file = File('$cachePath/$key');
      Map<String, dynamic> existingData = {};

      //获取到已经存在的 Json
      if (await file.exists()) {
        final jsonString = await file.readAsString();
        existingData = jsonDecode(jsonString) as Map<String, dynamic>;
      }

      //存入现有的 key - value 缓存
      existingData[key] = {
        'data': jsonData,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'expiration': expiration?.inMilliseconds,
      };

      //转化为新的Json文本
      String newJsonString = jsonEncode(existingData);

      //Base64加密内容
      newJsonString = EncryptUtil.encodeBase64(newJsonString);

      //检查限制的总大小并写入到文件
      checkAndWriteFile(file, newJsonString);
    });
  }

顺便我们在存入 Json 数据的时候,转换为字符串加密为 Base64 的字符串。

【Flutter】更进一步,文件缓存管理的MaxSize,LRU,加密等功能实现

取出值的时候也只需要在读取文件字符串的方法中统一的处理即可:

  Future<Map<String, dynamic>?> _readAllJsonFromFile(String key) async {
    String path = '$cachePath/$key';
    final file = File(path);
    if (await file.exists()) {
      String jsonString = await file.readAsString();

      //解密Base64内容
      jsonString = EncryptUtil.decodeBase64(jsonString);

      final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;
      return jsonData;
    }
    return null;
  }

如果大家有自定义加密的逻辑,或者想要加盐加密的方式,都可以自行替换。

【Flutter】更进一步,文件缓存管理的MaxSize,LRU,加密等功能实现

后记

对于我们应用储存了很多全国地址信息,信息文章,培训信息等超大的文件这种场景,文件缓存管理是很有用的。

本文代码很多配置都是写死了,由于暂时并没有开源发布到 Pub 的打算,所以并没有做属性抽离与配置的一些逻辑,如果大家有需求可以复制源码自行修改。

本文的整体代码结合前文的文件缓存修改而成,核心代码文章中已经全部贴出。有兴趣可以自取(全部代码太大了,我并没有上传到 git)

那么大家都是如何实现的呢,小子抛砖引玉如果有更多的更好的其他方式也希望大家能评论区指导、交流一起学习进步。

当然如果代码、注释、解释有不到位或错漏的地方,希望同学们可以指出。

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

【Flutter】更进一步,文件缓存管理的MaxSize,LRU,加密等功能实现