数据结构 - LruCache和DiskLruCache

272 阅读9分钟

基本实现

1. LruCache 底层实现

1.1 核心结构

public class LruCache<K, V> {
    // 基于LinkedHashMap实现
    private final LinkedHashMap<K, V> map;
    private int size;
    private int maxSize;
    
    public LruCache(int maxSize) {
        // 创建访问顺序的LinkedHashMap
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        this.maxSize = maxSize;
    }
}
class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private final int capacity;
    
    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);  // 按访问顺序
        this.capacity = capacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > capacity;  // 超过容量则移除最老的
    }
}
graph LR
    A[LruCache] --> B[LinkedHashMap]
    B --> C[HashMap存储]
    B --> D[双向链表维序]
    
    subgraph "访问顺序"
    E[最近访问] --> F[较早访问] --> G[最早访问]
    end

1.2 工作原理图解

// 示例:maxSize = 4
LruCache<String, Bitmap> cache = new LruCache<>(4);

// 1. 添加元素
cache.put("A", bitmapA);  // [A]
cache.put("B", bitmapB);  // [B -> A]
cache.put("C", bitmapC);  // [C -> B -> A]
cache.put("D", bitmapD);  // [D -> C -> B -> A]

// 2. 访问B
cache.get("B");           // [B -> D -> C -> A]

// 3. 添加新元素E(超出大小)
cache.put("E", bitmapE);  // [E -> B -> D -> C]
                          // A被移除(最久未使用)
1. 基本概念

LRU(Least Recently Used)最近最少使用算法,简单来说就是:

  • 最近使用的数据会被缓存在内存中
  • 当缓存满时,会优先淘汰最久未使用的数据

想象一个简单的场景:

假设有一个大小为3的储物柜:

1. 先后放入 AB、C
[A] -> [B] -> [C]

2. 访问了一次 AA会被移到最前面
[B] -> [C] -> [A]

3. 现在要放入 D,柜子满了,会移除最久未使用的 B
[C] -> [A] -> [D]
2. 核心实现

LruCache 的核心是 LinkedHashMap,我们来看其构造:

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
    
    public LruCache(int maxSize) {
        // 最关键的构造参数
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
        this.maxSize = maxSize;
    }
}

这里的 LinkedHashMap 参数说明:

  • 第一个参数:初始容量
  • 第二个参数:负载因子
  • 第三个参数:accessOrder=true,表示按访问顺序排序
3. 数据结构图解
graph LR
    subgraph LinkedHashMap内部结构
    Head --> Node1
    Node1 --> Node2
    Node2 --> Node3
    Node3 --> Tail
    end

每个节点包含:

class Entry<K,V> {
    K key;
    V value;
    Entry<K,V> before;  // 双向链表前指针
    Entry<K,V> after;   // 双向链表后指针
    Entry<K,V> next;    // HashMap链表指针
}
4. 核心操作流程
4.1 get操作
public final V get(K key) {
    V mapValue;
    synchronized (this) {
        // 从map中获取值
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;  // 命中计数
            return mapValue;
        }
        missCount++;    // 未命中计数
    }

    // 创建值
    V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    // 放入缓存
    put(key, createdValue);
    return createdValue;
}

流程图:

graph TD
    A[获取值] --> B{是否存在?}
    B -->|是| C[更新位置]
    B -->|否| D[创建新值]
    D --> E[放入缓存]
    C --> F[返回值]
    E --> F
4.2 put操作
public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
        putCount++;
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
        // 关键步骤:清理旧数据
        trimToSize(maxSize);
    }
    return previous;
}
5. 实际使用示例

图片缓存的例子:

// 创建一个图片缓存
private static final int MAX_MEMORY = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final int CACHE_SIZE = MAX_MEMORY / 8;

LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(CACHE_SIZE) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // 重写sizeOf方法,计算bitmap的大小(单位KB)
        return bitmap.getByteCount() / 1024;
    }
};

// 存储图片
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap);
    }
}

// 获取图片
public Bitmap getBitmapFromMemCache(String key) {
    return memoryCache.get(key);
}
6. 性能优化建议
  1. 合理设置缓存大小
// 建议缓存大小
int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
  1. 避免存储过大对象
// 存储前压缩
Bitmap bitmap = BitmapFactory.decodeResource(resources, resId);
Bitmap thumbBitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height);
  1. 及时清理
@Override
protected void onDestroy() {
    memoryCache.evictAll(); // 清空缓存
    super.onDestroy();
}
7. 小结

LruCache 的核心特点:

  1. 线程安全(synchronized)
  2. 容量控制(maxSize)
  3. 访问排序(LinkedHashMap)
  4. 自动清理(trimToSize)

使用场景:

  • 图片缓存
  • 网络请求缓存
  • 复杂计算结果缓存

这就是 LruCache 的核心原理,它通过 LinkedHashMap 实现了一个简单高效的内存缓存机制,在 Android 开发中被广泛应用于图片加载等场景。

2. DiskLruCache 底层实现

2.1 核心结构
public final class DiskLruCache {
    // 日志文件
    private final File journalFile;
    // 日志临时文件
    private final File journalFileTmp;
    // 日志备份文件
    private final File journalFileBackup;
    // 写入器
    private Writer journalWriter;
    // 缓存目录
    private final File directory;
}
graph TD
    A[DiskLruCache] --> B[journal文件]
    A --> C[缓存文件]
    
    B --> D[CLEAN]
    B --> E[DIRTY]
    B --> F[REMOVE]
    
    C --> G[.0文件]
    C --> H[.1文件]
2.2 Journal文件结构
// journal文件内容示例
libcore.io.DiskLruCache    // 文件头
1                          // 版本号
100                        // 应用版本
2                          // 每个key对应的文件数

DIRTY 3400330d1dfc7f3f     // 开始写入
CLEAN 3400330d1dfc7f3f 832 // 写入完成
READ 3400330d1dfc7f3f      // 读取操作
REMOVE 3400330d1dfc7f3f    // 移除操作
2.3 写入过程
// 1. 创建Editor
DiskLruCache.Editor editor = cache.edit(key);

// 2. 获取输出流
OutputStream os = editor.newOutputStream(0);

// 3. 写入数据
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);

// 4. 提交
editor.commit();

// 日志记录过程:
// DIRTY key    // 开始写入
// CLEAN key    // 写入成功
// 或
// REMOVE key   // 写入失败
graph TD
    A[写入请求] --> B[创建Editor]
    B --> C[获取输出流]
    C --> D[写入数据]
    D --> E{提交}
    E -->|成功| F[CLEAN]
    E -->|失败| G[REMOVE]

3. 具体实现示例

3.1 LruCache 实现
public class BitmapCache extends LruCache<String, Bitmap> {
    // 计算内存
    private int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        return maxMemory / 8;
    }
    
    public BitmapCache() {
        super(getDefaultLruCacheSize());
    }
    
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // 计算图片大小
        return bitmap.getByteCount() / 1024;
    }
    
    @Override
    protected void entryRemoved(boolean evicted, String key,
            Bitmap oldValue, Bitmap newValue) {
        // 当图片被移除时回调
        if (evicted) {
            // 可以在这里做一些清理工作
            oldValue.recycle();
        }
    }
}
3.2 DiskLruCache 实现
public class DiskBitmapCache {
    private DiskLruCache mDiskCache;
    
    public DiskBitmapCache(Context context) {
        // 获取缓存目录
        File cacheDir = context.getCacheDir();
        // 打开缓存
        mDiskCache = DiskLruCache.open(
            cacheDir,
            1,              // 版本号
            1,              // 每个key对应的文件数
            10 * 1024 * 1024 // 10MB
        );
    }
    
    // 写入缓存
    public void putBitmap(String url, Bitmap bitmap) {
        String key = hashKeyForDisk(url);
        DiskLruCache.Editor editor = null;
        try {
            editor = mDiskCache.edit(key);
            if (editor != null) {
                OutputStream out = editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                editor.commit();
            }
        } catch (IOException e) {
            try {
                editor.abort();
            } catch (IOException ignored) {
            }
        }
    }
    
    // 读取缓存
    public Bitmap getBitmap(String url) {
        String key = hashKeyForDisk(url);
        try {
            DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
            if (snapshot != null) {
                return BitmapFactory.decodeStream(
                    snapshot.getInputStream(0));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

4. 形象比喻

想象一个图书管理系统:

  1. LruCache(内存缓存)

    • 图书馆的"快速服务台"
    • 有一个电子系统(LinkedHashMap)
    • 记录每本书的借阅情况
    • 自动将最久未借阅的书移到储藏室
  2. DiskLruCache(磁盘缓存)

    • 图书馆的"储藏室系统"
    • 有一个借阅日志(journal文件)
    • 记录每本书的存取状态
    • 定期清理长期未借阅的书

这样的设计让应用能够:

  1. 快速访问常用数据(内存缓存)
  2. 持久保存大量数据(磁盘缓存)
  3. 自动管理缓存大小
  4. 保证数据的一致性

就像一个高效的图书管理系统!

DiskLruCache的详细讲解

1. 基本概念

想象 DiskLruCache 是一个文件管理员:

DiskLruCache cache = DiskLruCache.open(
    new File("缓存文件夹"),    // 管理员的办公室
    1,                        // 版本号(规章制度版本)
    1,                        // 每个文件的分块数
    10 * 1024 * 1024         // 最大容量(10MB)
);

2. 文件结构

缓存目录/
├── journal        // 记录本:记录所有操作
├── 1.tmp         // 临时文件:正在写入的文件
├── 1.0          // 缓存文件:已经写入完成的文件
├── 2.0          // 缓存文件:另一个文件
└── 3.0          // 缓存文件:第三个文件

3. Journal文件(记录本)

// journal 文件内容
libcore.io.DiskLruCache   // 第一行:文件类型
1                         // 第二行:版本号
100                       // 第三行:应用版本号
2                         // 第四行:每个key的文件数

// 操作记录:
DIRTY abc123             // 开始写入
CLEAN abc123 1234        // 写入成功
READ abc123              // 读取文件
REMOVE abc123            // 删除文件

就像管理员的工作日志本!

4. 写入过程

// 1. 请求写入许可
DiskLruCache.Editor editor = cache.edit(key);

try {
    // 2. 获取文件输出流
    OutputStream out = editor.newOutputStream(0);
    
    // 3. 写入数据
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
    
    // 4. 提交写入
    editor.commit();
} catch (IOException e) {
    // 5. 写入失败,撤销
    editor.abort();
}

形象比喻:

graph TD
    A[请求写入] --> B[创建临时文件]
    B --> C[写入数据]
    C --> D{是否成功?}
    D -->|成功| E[变成正式文件]
    D -->|失败| F[删除临时文件]

bitmap.compress的详细解释:

// 基本语法
bitmap.compress(Bitmap.CompressFormat format, int quality, OutputStream stream)

参数说明:

1. CompressFormat format: 压缩格式
   - JPEG:有损压缩,文件较小,不支持透明度
   - PNG:无损压缩,文件较大,支持透明度
   - WEBP:Google开发的格式,同时支持有损和无损压缩

2. int quality: 压缩质量
   - 取值范围:0-100
   - 100:最高质量,最小压缩
   - 0:最低质量,最大压缩

3. OutputStream stream: 输出流
   - 用于指定压缩后图片的输出位置

使用示例:

// 1. 保存到文件
try {
    FileOutputStream fos = new FileOutputStream(new File("图片路径.jpg"));
    bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
    fos.flush();
    fos.close();
} catch (IOException e) {
    e.printStackTrace();
}

// 2. 转换为字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] bytes = baos.toByteArray();

// 3. 压缩图片大小
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int quality = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
// 如果大小超过500KB,继续压缩
while (baos.toByteArray().length / 1024 > 500) {  
    baos.reset();
    quality -= 10;
    bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos);
}

常见使用场景:

1. 上传图片前压缩
   - 减小图片体积
   - 节省网络流量
   - 加快上传速度

2. 保存图片到本地
   - 存储到手机存储
   - 控制存储空间使用

3. 图片格式转换
   - 从PNG转为JPEG
   - 从WEBP转为其他格式

注意事项:

1. 压缩是不可逆的
   - 一旦压缩后无法恢复原始质量

2. 选择合适的格式
   - 需要透明度用PNG
   - 普通照片用JPEG
   - 追求更好压缩比可以用WEBP

3. quality参数的选择
   - 建议不要低于60,否则图片质量可能明显下降
   - 可以通过循环逐步降低quality直到达到目标大小

5. 读取过程

// 1. 查找缓存
DiskLruCache.Snapshot snapshot = cache.get(key);

if (snapshot != null) {
    // 2. 获取输入流
    InputStream in = snapshot.getInputStream(0);
    
    // 3. 读取数据
    Bitmap bitmap = BitmapFactory.decodeStream(in);
}

就像查找文件:

graph TD
    A[查找文件] --> B{文件存在?}
    B -->|是| C[打开文件]
    B -->|否| D[返回null]
    C --> E[读取内容]

editor.commit()editor.abort() 都是同步操作。让我详细解释一下:

// DiskLruCache的典型使用模式
DiskLruCache.Editor editor = cache.edit(key);
try {
    // 同步写入操作
    OutputStream out = editor.newOutputStream(0);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
    out.close(); // 建议在commit前关闭流
    
    // commit()是同步操作
    editor.commit();  // 会立即将文件从临时文件重命名为正式缓存文件
} catch (IOException e) {
    // abort()也是同步操作
    editor.abort();   // 会立即删除临时文件
}

内部实现原理:

// DiskLruCache内部实现(简化版)
public void commit() throws IOException {
    // 1. 检查编辑器状态
    if (hasErrors) {
        completeEdit(this, false);
        remove(entry.key); // 删除该条目
    } else {
        completeEdit(this, true);
    }
}

public void abort() throws IOException {
    completeEdit(this, false);
}

private synchronized void completeEdit(Editor editor, boolean success) {
    // 同步方法,直接进行文件操作
    if (success) {
        // 将临时文件重命名为正式文件
        File dirty = entry.getDirtyFile(i);
        File clean = entry.getCleanFile(i);
        dirty.renameTo(clean);
    } else {
        // 删除临时文件
        deleteIfExists(entry.getDirtyFile(i));
    }
}

重要说明:

  1. 操作是同步的

    • 这些操作会直接操作文件系统
    • 会阻塞当前线程直到操作完成
  2. 建议的使用方式

// 推荐在异步线程中使用
new Thread(() -> {
    DiskLruCache.Editor editor = cache.edit(key);
    try {
        OutputStream out = editor.newOutputStream(0);
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
        out.close();
        editor.commit();
    } catch (IOException e) {
        editor.abort();
    }
}).start();
  1. 注意事项
// 1. 确保正确关闭流
OutputStream out = editor.newOutputStream(0);
try {
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
    out.close(); // 重要!
    editor.commit();
} catch (IOException e) {
    editor.abort();
} finally {
    try {
        out.close();
    } catch (IOException e) {
        // 忽略
    }
}

// 2. commit和abort只能调用一次
editor.commit(); // 第一次调用有效
editor.commit(); // 会抛出IllegalStateException
  1. 性能考虑
// 由于是同步IO操作,建议:
1. 在异步线程中执行
2. 不要在主线程中执行
3. 可以考虑使用线程池管理这些IO操作

6. 实际例子:图片缓存

public class ImageDiskCache {
    private DiskLruCache diskCache;
    
    // 存储图片
    public void saveImage(String url, Bitmap bitmap) {
        String key = hashKeyForDisk(url);  // 生成文件名
        
        try {
            // 1. 开始写入
            DiskLruCache.Editor editor = diskCache.edit(key);
            if (editor != null) {
                // 2. 写入图片数据
                OutputStream out = editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
                // 3. 提交
                editor.commit();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    // 读取图片
    public Bitmap loadImage(String url) {
        String key = hashKeyForDisk(url);
        
        try {
            // 1. 查找缓存
            DiskLruCache.Snapshot snapshot = diskCache.get(key);
            if (snapshot != null) {
                // 2. 读取图片
                InputStream in = snapshot.getInputStream(0);
                return BitmapFactory.decodeStream(in);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

7. 生动比喻

想象一个图书馆管理系统:

  1. Journal文件 = 图书管理日志

    • 记录每本书的借出、归还
    • 记录新书入库、旧书清理
    • 保证数据的完整性
  2. 缓存文件 = 图书

    • 临时文件(.tmp) = 正在入库的新书
    • 正式文件(.0) = 已入库的书
    • 删除文件 = 清理旧书
  3. 操作过程 = 图书管理流程

    • DIRTY = 开始处理新书
    • CLEAN = 新书入库完成
    • READ = 借阅图书
    • REMOVE = 清理旧书