在Android开发中,缓存是提高应用性能的重要手段。Android提供了两种基于LRU(Least Recently Used)算法的缓存实现:
- LruCache:内存缓存实现
- DiskLruCache:磁盘缓存实现
这两种缓存都遵循LRU算法,即当缓存满时,优先移除最近最少使用的缓存项。
一、核心原理剖析
1. LruCache(内存缓存)
实现原理:
LruCache是基于LinkedHashMap实现的,它维护了一个双向链表来记录访问顺序:
- 每次访问一个条目时,该条目会被移动到链表的头部
- 当缓存满时,链表尾部的条目(最近最少使用的)会被移除
实现机制:
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/8
- 重写sizeOf()方法准确计算每个条目的大小
- 避免存储大对象导致频繁GC
- 考虑实现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提供的一个开源实现。它的工作原理:
- 使用日志文件(journal)记录所有缓存操作
- 缓存文件存储在文件系统的特定目录
- 每个缓存条目对应一个或多个文件
- 使用LRU算法管理磁盘空间
核心特性:
- 持久化存储:缓存会保留在磁盘上
- 线程安全:所有操作都是同步的
- 可配置最大尺寸:基于字节数
- 支持每个条目多个文件
- 提供编辑和快照机制
文件结构:
kotlin
/cache
|- journal // 操作日志
|- 0d5c1f2a.data // 数据文件
|- 0d5c1f2a.meta // 元数据
日志文件格式:
objectivec
LIBERATION_HEADER
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313b52
REMOVE 335c4c6028171cfddfbaae1a9c313b52
READ 335c4c6028171cfddfbaae1a9c313b52
使用场景:
- 网络响应缓存
- 图片的磁盘缓存
- 需要长期保存的临时数据
- 大数据量的缓存
注意事项:
- 合理设置缓存目录,通常使用context.getCacheDir()
- 设置适当的缓存大小,考虑用户设备存储空间
- 定期调用flush()确保数据写入磁盘
- 考虑实现版本控制,缓存格式变更时能自动清除旧缓存
- 注意文件操作可能导致的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:提供持久的磁盘缓存,适合存储大数据或需要持久化的数据
- 结合使用:形成二级缓存体系,兼顾速度和存储容量
在实际开发中,应根据具体需求选择合适的缓存策略,并注意缓存的管理和优化,以提供最佳的用户体验。