flutter实现LRU图片缓存策略

43 阅读3分钟

在一些以图片为主的app中,常用页面一般都包含了很多网络图片,所以图片的显示速度非常重要,直接跟用户的体验挂钩。可以通过缓存图片的方法将图片保存下来,这样子除了第一次需要等待下载,后续都能够直接显示,就像本地图片一样。

但是随着图片的缓存越来越多,app的体积也会越来越大,所以需要定期或者定量的删除一些没有用或者不怎么用的图片缓存,这里用到的是LRU策略,以下是它的介绍:

image.png

以下是我根据这个解释,自己想到的一个flutter实现思路:

  1. 定义一个临时的有序数组,用来存储当前缓存的文件名称;
  2. 定义一个映射缓存字段,用来根据文件名去找到真正的缓存路径,如下图所示; image.png
  3. 创建一个等待队列,把每张图片添加到临时数组的方法,通过队列的方式依次执行,确保不会出现竞态的问题;
  4. 每次有图片显示时,先查找这张图片都文件名是否已经出现在临时数组里,如果有的话需要把它“挪移”到最前面,没有到话直接在最前面添加就可以;
  5. 当添加完之后,检查当前的临时数组长度是否大于最大缓存数,如果大于了,需要将超过的部分删除掉,必须同时删除临时数组、映射缓存字段、图片实际缓存;
  6. 应用退出时,把临时数组保存到缓存中,确保下一次打开应用时,能衔接上去;

以下是实现的代码

LRU服务代码
import 'dart:async';
import 'dart:collection';
import 'dart:io';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:path_provider/path_provider.dart';

import '../utils/storage.dart';

class LruImageService {
  static List<dynamic> imageNameList = [];  // 文件名数组
  static final Queue<Future<void> Function()> _operationQueue = Queue();  // 操作队列(存储待执行的增删操作)
  static final int _maxCount = int.parse(dotenv.env['IMAGE_CACHE_MAX'].toString());  // 最大缓存数量(超过此值会清理)
  static bool _isProcessing = false;   // 是否正在执行队列中的操作(避免并发执行)
  static String _directory = "";   // 文件目录

  // 初始化
  static void init(){
    // 获取应用目录路径
    _getDirectory();
    // 初始化数组
    _initList();
  }

  static void _getDirectory() async {
    var directory = await StorageUtil.get(StorageUtil.APP_DIRECTORY);
    if(directory != null){
      _directory = directory;
    }else{
      directory = await getApplicationDocumentsDirectory();
      StorageUtil.save(StorageUtil.APP_DIRECTORY, directory.path);
      _directory = directory.path;
    }
  }

  /// 初始化缓存数组
  static Future<void> _initList() async {
    imageNameList = await StorageUtil.get(StorageUtil.LRU_IMAGE_LIST, []);
  }

  /*
  * 新增数据(只接收网络图片)
  * 1. 提取文件名
  * 2. 判断文件名数组(imageNameList)中是否有该图片数据,有的话把该文件名位置挪到第一位,没有的话直接在数组头部添加该文件名
  * */
  static void insertData(String fileName) {
    // 将“新增操作”包装成Future,加入队列
    _operationQueue.add(() async {
      // 判断文件名数组中是否有该数据
      bool hasData = imageNameList.contains(fileName);
      if (hasData) {
        // 有的话,直接删除掉
        imageNameList.remove(fileName);
      }
      // 最后统一插入到最前面
      imageNameList.insert(0, fileName);
      // 判断添加完成后,是否超过最大显示,超过的话,就删除最后一个数据
      if (imageNameList.length > _maxCount) {
        await deleteFileByName(imageNameList[imageNameList.length - 1]);
        imageNameList.removeLast();
      }
    });
    // 触发队列处理
    _processQueue();
  }

  // 根据图片名删除图片
  static Future<void> deleteFileByName(String fileName) async {
    String filePath = await StorageUtil.get(fileName, "");
    if (filePath != "") {
      try {
        filePath = filePath.replaceAll("file_image_name:", "$_directory/");
        final file = File(filePath);
        // 检查文件是否存在
        if (await file.exists()) {
          await file.delete();
          StorageUtil.remove(fileName);
          print("删除了$filePath");
        } else {
          print('文件不存在:$filePath');
        }
      } catch (e) {
        print('删除文件失败:$e');
      }
    } else {
      print("文件$fileName不存在");
    }
  }

  /// 处理队列(核心逻辑)
  static Future<void> _processQueue() async {
    // 若正在处理或队列为空,直接返回
    if (_isProcessing || _operationQueue.isEmpty) return;

    _isProcessing = true;
    try {
      // 取出队列中的第一个操作并执行
      final operation = _operationQueue.removeFirst();
      await operation(); // 等待当前操作完成
    } catch (e) {
      print('操作执行失败:$e');
    } finally {
      _isProcessing = false;
      // 递归处理剩余操作(直到队列为空)
      _processQueue();
    }
  }

  /// 保存到缓存
  static Future<void> saveFileNameList() async {
    await StorageUtil.save(StorageUtil.LRU_IMAGE_LIST, imageNameList);
  }
}
初始化
LruImageService.init();  // 图片缓存策略初始化
应用退出时
LruImageService.saveFileNameList();
实际使用
LruImageService.insertData(fileName);