Android LruCache 与 DiskLruCache 深度解析

在Android开发中,缓存是提高应用性能的重要手段。Android提供了两种基于LRU(Least Recently Used)算法的缓存实现:

  • LruCache:内存缓存实现
  • DiskLruCache:磁盘缓存实现

这两种缓存都遵循LRU算法,即当缓存满时,优先移除最近最少使用的缓存项。

一、核心原理剖析

1. LruCache(内存缓存)

实现原理:

LruCache是基于LinkedHashMap实现的,它维护了一个双向链表来记录访问顺序:

  1. 每次访问一个条目时,该条目会被移动到链表的头部
  2. 当缓存满时,链表尾部的条目(最近最少使用的)会被移除

实现机制:

java 复制代码
// 核心数据结构 = LinkedHashMap(accessOrder=true)
val map = object : LinkedHashMap<K, V>(initialCapacity, loadFactor, true) {
    override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean {
        return size > maxSize
    }
}

// 操作流程:
put → 触发eldest检查 → 若超限则移除最旧条目
get → 调整链表顺序(最近访问项移至队尾)

关键参数:

kotlin 复制代码
// 建议设置(以Bitmap缓存为例)
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8 // 通常分配1/8内存

核心特性:

  • 线程安全:所有操作都是同步的
  • 自动回收:当缓存满时自动移除最旧的条目
  • 可配置最大尺寸:基于条目数量或总大小
  • 条目移除通知:可监听条目被移除的事件

使用场景:

  • 图片缓存(如Bitmap)
  • 频繁访问的数据对象
  • 计算成本高的结果缓存

注意事项:

  1. 合理设置缓存大小,通常为可用内存的1/8
  2. 重写sizeOf()方法准确计算每个条目的大小
  3. 避免存储大对象导致频繁GC
  4. 考虑实现entryRemoved()处理条目被移除的情况

示例代码:

java 复制代码
// 获取系统分配给应用的最大内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用1/8作为缓存大小
int cacheSize = maxMemory / 8;

LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // 计算Bitmap占用的内存大小,单位KB
        return bitmap.getByteCount() / 1024;
    }
    
    @Override
    protected void entryRemoved(boolean evicted, String key, 
            Bitmap oldValue, Bitmap newValue) {
        // 条目被移除时的处理,如回收Bitmap
        if (oldValue != null && !oldValue.isRecycled()) {
            oldValue.recycle();
        }
    }
};

// 添加缓存
memoryCache.put("key1", bitmap1);

// 获取缓存
Bitmap bitmap = memoryCache.get("key1");

// 移除缓存
memoryCache.remove("key1");

2. DiskLruCache(磁盘缓存)

实现原理:

DiskLruCache不是Android SDK的一部分,而是由Jake Wharton在JakeWharton/DiskLruCache提供的一个开源实现。它的工作原理:

  1. 使用日志文件(journal)记录所有缓存操作
  2. 缓存文件存储在文件系统的特定目录
  3. 每个缓存条目对应一个或多个文件
  4. 使用LRU算法管理磁盘空间

核心特性:

  • 持久化存储:缓存会保留在磁盘上
  • 线程安全:所有操作都是同步的
  • 可配置最大尺寸:基于字节数
  • 支持每个条目多个文件
  • 提供编辑和快照机制

文件结构

kotlin 复制代码
/cache
|- journal      // 操作日志
|- 0d5c1f2a.data // 数据文件
|- 0d5c1f2a.meta // 元数据

日志文件格式

objectivec 复制代码
LIBERATION_HEADER
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313b52
REMOVE 335c4c6028171cfddfbaae1a9c313b52
READ 335c4c6028171cfddfbaae1a9c313b52

使用场景:

  • 网络响应缓存
  • 图片的磁盘缓存
  • 需要长期保存的临时数据
  • 大数据量的缓存

注意事项:

  1. 合理设置缓存目录,通常使用context.getCacheDir()
  2. 设置适当的缓存大小,考虑用户设备存储空间
  3. 定期调用flush()确保数据写入磁盘
  4. 考虑实现版本控制,缓存格式变更时能自动清除旧缓存
  5. 注意文件操作可能导致的ANR

示例代码:

gradle 复制代码
implementation 'com.jakewharton:disklrucache:2.0.2'
java 复制代码
// 初始化DiskLruCache
File cacheDir = new File(context.getCacheDir(), "my_cache");
if (!cacheDir.exists()) {
    cacheDir.mkdirs();
}
int appVersion = 1; // 当缓存结构变化时递增此值
int valueCount = 1; // 每个key对应的文件数
long maxSize = 10 * 1024 * 1024; // 10MB

DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, appVersion, valueCount, maxSize);

// 写入缓存
String key = "unique_key"; // 通常使用URL的hash等
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(0);
    try {
        // 写入数据到outputStream
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
        editor.commit();
    } catch (IOException e) {
        editor.abort();
        e.printStackTrace();
    } finally {
        closeQuietly(outputStream);
    }
}

// 读取缓存
DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
if (snapshot != null) {
    InputStream inputStream = snapshot.getInputStream(0);
    try {
        Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
        // 使用bitmap
    } finally {
        closeQuietly(inputStream);
        snapshot.close();
    }
}

// 移除缓存
diskLruCache.remove(key);

// 关闭缓存(通常在onDestroy中调用)
try {
    diskLruCache.close();
} catch (IOException e) {
    e.printStackTrace();
}

// 删除所有缓存
diskLruCache.delete();

3. 结合使用LruCache和DiskLruCache

在实际应用中,通常会将两者结合使用,形成二级缓存:

java 复制代码
public class ImageCache {
    private LruCache<String, Bitmap> memoryCache;
    private DiskLruCache diskLruCache;
    
    public ImageCache(Context context) {
        // 初始化内存缓存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        memoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
            }
        };
        
        // 初始化磁盘缓存
        try {
            File cacheDir = getDiskCacheDir(context, "bitmap");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            diskLruCache = DiskLruCache.open(cacheDir, 1, 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public void addBitmapToCache(String key, Bitmap bitmap) {
        // 添加到内存缓存
        if (getBitmapFromMemCache(key) == null) {
            memoryCache.put(key, bitmap);
        }
        
        // 添加到磁盘缓存
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(0);
                if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
                    editor.commit();
                } else {
                    editor.abort();
                }
                closeQuietly(outputStream);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public Bitmap getBitmapFromCache(String key) {
        // 先从内存缓存获取
        Bitmap bitmap = getBitmapFromMemCache(key);
        if (bitmap != null) {
            return bitmap;
        }
        
        // 再从磁盘缓存获取
        try {
            DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
            if (snapshot != null) {
                InputStream inputStream = snapshot.getInputStream(0);
                bitmap = BitmapFactory.decodeStream(inputStream);
                closeQuietly(inputStream);
                snapshot.close();
                
                // 将磁盘缓存添加到内存缓存
                if (bitmap != null) {
                    memoryCache.put(key, bitmap);
                }
                return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        return null;
    }
    
    private Bitmap getBitmapFromMemCache(String key) {
        return memoryCache.get(key);
    }
    
    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }
    
    private void closeQuietly(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二、典型使用场景对比

维度 LruCache DiskLruCache
数据特性 小对象(Bitmap、JSON等) 大文件(图片、视频、数据库备份等)
访问速度 纳秒级 毫秒级
生命周期 随进程销毁 持久化存储
典型应用 界面资源缓存 离线数据缓存
内存/磁盘开销 占用堆内存 占用存储空间

三、最佳实践

1. 合理设置缓存大小

  • 内存缓存:通常为可用内存的1/8
  • 磁盘缓存:考虑设备存储空间,通常10-50MB

2. 缓存键设计

  • 使用唯一且稳定的键(如URL的hash)
  • 考虑添加版本信息

3. 缓存策略

  • 优先从内存缓存获取
  • 内存缓存未命中再从磁盘缓存获取
  • 都未命中再从网络加载

4. 缓存清理

  • 在应用启动时检查磁盘缓存大小
  • 在低存储空间时主动清理缓存
  • 提供手动清理缓存的选项

5. 性能优化

  • 磁盘缓存操作在后台线程执行
  • 批量操作减少磁盘I/O
  • 定期调用flush()但不要太频繁

6. 错误处理

  • 处理磁盘操作可能抛出的IOException
  • 缓存失败时应有回退机制

四、常见问题及解决方案

1. 内存缓存命中率低

  • 检查缓存大小是否设置合理
  • 分析键的设计是否合理
  • 考虑增加缓存大小

2. 磁盘缓存性能差

  • 确保磁盘操作在后台线程
  • 减少小文件的频繁读写
  • 考虑使用缓冲流

3. 缓存不一致

  • 实现版本控制
  • 提供强制清除缓存的机制
  • 添加缓存校验(如hash校验)

4. 缓存导致OOM

  • 确保正确计算条目大小
  • 对大图片进行适当缩放
  • 实现内存缓存条目的回收

五、总结

LruCache和DiskLruCache是Android中实现高效缓存的重要工具,合理使用它们可以显著提升应用性能:

  • LruCache:提供快速的内存缓存,适合存储频繁访问的小数据
  • DiskLruCache:提供持久的磁盘缓存,适合存储大数据或需要持久化的数据
  • 结合使用:形成二级缓存体系,兼顾速度和存储容量

在实际开发中,应根据具体需求选择合适的缓存策略,并注意缓存的管理和优化,以提供最佳的用户体验。

更多分享

  1. Android图片加载篇: Glide 缓存机制深度优化指南
  2. Android 冷启动优化实践:含主线程优化、资源预加载与懒加载、跨进程预热等
  3. Android ContentProvider 详解及结合 Jetpack Startup 的优化实践
相关推荐
zepcjsj080134 分钟前
简单实现支付密码的页面及输入效果
android
小阳睡不醒2 小时前
小白成长之路-部署Zabbix7(二)
android·运维
mmoyula3 小时前
【RK3568 PWM 子系统(SG90)驱动开发详解】
android·linux·驱动开发
你过来啊你6 小时前
Android用户鉴权实现方案深度分析
android·鉴权
Kiri霧8 小时前
IntelliJ IDEA
java·ide·kotlin·intellij-idea
kerli8 小时前
Android 嵌套滑动设计思想
android·客户端
恣艺9 小时前
LeetCode 854:相似度为 K 的字符串
android·算法·leetcode
阿华的代码王国9 小时前
【Android】相对布局应用-登录界面
android·xml·java
用户2070386194910 小时前
StateFlow与SharedFlow如何取舍?
android