深入浅出图片三级缓存
一、什么是图片三级缓存?——快递驿站体系
想象图片加载就像网购快递:
- 内存缓存:你家门口的快递柜(取货最快)
- 磁盘缓存:小区快递驿站(取货稍慢)
- 网络加载:从外地仓库发货(最慢)
三级缓存就是建立这样一套高效配送体系,避免每次都从远方仓库取货。
二、各级缓存详解
1. 内存缓存(L1缓存)——家门口的快递柜
特点:
- 读取速度:纳秒级(堪比CPU缓存)
- 存储介质:RAM内存
- 容量限制:通常为可用内存的1/8
- 淘汰策略:LRU(最近最少使用)
// Android实现示例(LruCache)
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024 / 8);
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
};
2. 磁盘缓存(L2缓存)——小区快递驿站
特点:
- 读取速度:毫秒级(比内存慢100倍)
- 存储介质:手机存储/SD卡
- 容量限制:通常10-100MB
- 文件格式:一般为图片原文件或编码后数据
// DiskLruCache实现示例
File cacheDir = new File(context.getCacheDir(), "image_cache");
int cacheSize = 50 * 1024 * 1024; // 50MB
DiskLruCache diskCache = DiskLruCache.open(cacheDir, 1, 1, cacheSize);
3. 网络加载(L3缓存)——远方仓库
特点:
- 加载速度:秒级(受网络影响大)
- 成本最高:消耗流量、电量
- 需要异步处理:避免阻塞UI线程
// 使用OkHttp网络请求
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(imageUrl).build();
client.newCall(request).enqueue(new Callback() {
public void onResponse(Call call, Response response) {
// 获取图片数据并缓存
}
});
三、完整工作流程
-
检查内存缓存:
Bitmap bitmap = memoryCache.get(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; // 命中缓存直接返回 } -
检查磁盘缓存:
DiskLruCache.Snapshot snapshot = diskCache.get(imageKey); if (snapshot != null) { InputStream inputStream = snapshot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(inputStream); // 存入内存缓存 memoryCache.put(imageKey, bitmap); imageView.setImageBitmap(bitmap); return; } -
从网络加载:
// 使用线程池或协程异步加载 executorService.execute(() -> { Bitmap bitmap = downloadFromNetwork(imageUrl); // 存入磁盘和内存缓存 diskCache.put(imageKey, bitmap); memoryCache.put(imageKey, bitmap); // 更新UI(需切主线程) runOnUiThread(() -> imageView.setImageBitmap(bitmap)); });
四、关键技术点
1. 缓存键设计
- 通常使用图片URL的MD5值作为key
- 考虑图片尺寸差异(相同URL不同尺寸应视为不同缓存)
String generateKey(String url, int width, int height) {
String originalKey = url + "_" + width + "x" + height;
return md5(originalKey);
}
2. 图片压缩
- 根据ImageView尺寸进行采样压缩
- 使用RGB_565减少内存占用(适合不透明图片)
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeStream(inputStream, null, options);
3. 内存缓存优化
- 使用弱引用+强引用双缓存策略
- 监听onTrimMemory()及时清理缓存
public void onTrimMemory(int level) {
if (level >= TRIM_MEMORY_MODERATE) {
memoryCache.evictAll(); // 清空内存缓存
}
}
五、现成解决方案
1. Glide实现原理
Glide的三级缓存:
1. 活动资源(Active Resources):正在使用的图片
2. 内存缓存(Memory Cache):LRU缓存
3. 磁盘缓存(Disk Cache):转换后的图片
2. Picasso缓存策略
Picasso.with(context)
.load(url)
.memoryPolicy(MemoryPolicy.NO_CACHE) // 跳过内存缓存
.networkPolicy(NetworkPolicy.OFFLINE) // 只读磁盘缓存
.into(imageView);
六、性能对比数据
| 缓存级别 | 读取时间 | 存储容量 | 是否持久化 |
|---|---|---|---|
| 内存缓存 | 1-10ms | 10-50MB | 否 |
| 磁盘缓存 | 50-200ms | 50-200MB | 是 |
| 网络加载 | 500ms-5s | 无限制 | 是 |
七、常见问题解决方案
1. 图片错位问题
- 使用Tag验证:
imageView.setTag(imageUrl);
// 加载完成后检查
if (imageView.getTag().equals(imageUrl)) {
imageView.setImageBitmap(bitmap);
}
2. 缓存一致性问题
- 使用版本控制:
String key = url + "_v" + imageVersion;
3. OOM问题
- 采用以下策略:
- 合理设置缓存大小
- 使用Bitmap复用池
- 及时回收不再使用的Bitmap
八、总结
图片三级缓存就像高效的物流系统:
- 内存缓存:随取随用,但容量有限
- 磁盘缓存:持久存储,速度适中
- 网络加载:终极方案,成本最高
最佳实践原则:
- 优先读内存:速度最快
- 异步写磁盘:避免阻塞UI
- 网络最后用:节省流量
记住三个关键点:
- 缓存键要唯一:URL+尺寸等组合
- 及时释放资源:监听系统内存事件
- 合理设置大小:根据应用特点调整
掌握三级缓存,你的APP图片加载将又快又省流量!