深入浅出图片三级缓存

267 阅读4分钟

深入浅出图片三级缓存

一、什么是图片三级缓存?——快递驿站体系

想象图片加载就像网购快递:

  • 内存缓存:你家门口的快递柜(取货最快)
  • 磁盘缓存:小区快递驿站(取货稍慢)
  • 网络加载:从外地仓库发货(最慢)

三级缓存就是建立这样一套高效配送体系,避免每次都从远方仓库取货。

二、各级缓存详解

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) {
        // 获取图片数据并缓存
    }
});

三、完整工作流程

  1. 检查内存缓存

    Bitmap bitmap = memoryCache.get(imageKey);
    if (bitmap != null) {
        imageView.setImageBitmap(bitmap);
        return; // 命中缓存直接返回
    }
    
  2. 检查磁盘缓存

    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;
    }
    
  3. 从网络加载

    // 使用线程池或协程异步加载
    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-10ms10-50MB
磁盘缓存50-200ms50-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

八、总结

图片三级缓存就像高效的物流系统:

  1. 内存缓存:随取随用,但容量有限
  2. 磁盘缓存:持久存储,速度适中
  3. 网络加载:终极方案,成本最高

最佳实践原则:

  • 优先读内存:速度最快
  • 异步写磁盘:避免阻塞UI
  • 网络最后用:节省流量

记住三个关键点:

  1. 缓存键要唯一:URL+尺寸等组合
  2. 及时释放资源:监听系统内存事件
  3. 合理设置大小:根据应用特点调整

掌握三级缓存,你的APP图片加载将又快又省流量!