基本实现
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. 先后放入 A、B、C
[A] -> [B] -> [C]
2. 访问了一次 A,A会被移到最前面
[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. 性能优化建议
- 合理设置缓存大小
// 建议缓存大小
int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 8);
- 避免存储过大对象
// 存储前压缩
Bitmap bitmap = BitmapFactory.decodeResource(resources, resId);
Bitmap thumbBitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height);
- 及时清理
@Override
protected void onDestroy() {
memoryCache.evictAll(); // 清空缓存
super.onDestroy();
}
7. 小结
LruCache 的核心特点:
- 线程安全(synchronized)
- 容量控制(maxSize)
- 访问排序(LinkedHashMap)
- 自动清理(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. 形象比喻
想象一个图书管理系统:
-
LruCache(内存缓存)
- 图书馆的"快速服务台"
- 有一个电子系统(LinkedHashMap)
- 记录每本书的借阅情况
- 自动将最久未借阅的书移到储藏室
-
DiskLruCache(磁盘缓存)
- 图书馆的"储藏室系统"
- 有一个借阅日志(journal文件)
- 记录每本书的存取状态
- 定期清理长期未借阅的书
这样的设计让应用能够:
- 快速访问常用数据(内存缓存)
- 持久保存大量数据(磁盘缓存)
- 自动管理缓存大小
- 保证数据的一致性
就像一个高效的图书管理系统!
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));
}
}
重要说明:
-
操作是同步的
- 这些操作会直接操作文件系统
- 会阻塞当前线程直到操作完成
-
建议的使用方式
// 推荐在异步线程中使用
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. 确保正确关闭流
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
- 性能考虑
// 由于是同步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. 生动比喻
想象一个图书馆管理系统:
-
Journal文件 = 图书管理日志
- 记录每本书的借出、归还
- 记录新书入库、旧书清理
- 保证数据的完整性
-
缓存文件 = 图书
- 临时文件(.tmp) = 正在入库的新书
- 正式文件(.0) = 已入库的书
- 删除文件 = 清理旧书
-
操作过程 = 图书管理流程
- DIRTY = 开始处理新书
- CLEAN = 新书入库完成
- READ = 借阅图书
- REMOVE = 清理旧书