目录

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 的优化实践
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
264玫瑰资源库9 小时前
嘻游电玩三端客户端部署实战:PC + Android + iOS 环境全覆盖教程
android·ios
鸿蒙布道师9 小时前
鸿蒙NEXT开发权限工具类(申请授权相关)(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师9 小时前
鸿蒙NEXT开发定位工具类 (WGS-84坐标系)(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
alexhilton10 小时前
深入理解Jetpack Compose中的函数的执行顺序
android·kotlin·android jetpack
李新_11 小时前
Android 画中画避坑指北
android
一一Null12 小时前
Android studio—socketIO库return与emit的使用
android·java·网络·ide·websocket·网络协议·android studio
ansondroider13 小时前
Android RK356X TVSettings USB调试开关
android·adb·usb·otg·rk356x
全栈极简13 小时前
Android串口通信
android
jiaxingcode13 小时前
MAC系统下完全卸载Android Studio
android·macos·android studio
张力尹13 小时前
「架构篇 1」认识 MVC / MVP / MVVM / MVI
android·面试·架构