第2章 缓存类型与适用场景:内存缓存、磁盘缓存、网络缓存、二级缓存、三级缓存的选择原则

主线程缓存读写防 ANR 的核心挑战在于:在微秒级响应窗口内,选对数据源。不同缓存类型在延迟、容量、持久性和一致性上差异巨大,选错类型会导致主线程卡顿甚至 ANR。本章逐一拆解 5 种缓存类型的内部原理、适用边界,并给出可量化的选择原则。

2.1 缓存类型全景

从主线程视角看,缓存本质是用空间换时间的数据副本。按存储介质与访问路径,分为三个基础类型和两个组合类型:

类型存储介质典型延迟容量上限持久性ANR 敏感度
内存缓存堆内存 / Native 内存< 1 ms应用堆的 1/8 ~ 1/4进程结束即丢失极高(阻塞直接卡死)
磁盘缓存闪存 / SD 卡10 ~ 100 ms数 MB ~ 数百 MB跨进程/重启保留中(异步写可缓解)
网络缓存远程服务器100 ms ~ 数秒理论上无限服务端控制极高(网络抖动直接 ANR)
二级缓存内存 + 磁盘组合延迟两者之和部分持久中(命中率决定)
三级缓存内存 + 磁盘 + 网络组合延迟理论无限完全持久可选低(逐级降级)

2.2 内存缓存(Memory Cache)

原理:数据以对象引用形式驻留在应用进程堆或 Native 堆中,主线程通过指针直接读取,无 I/O 调度开销。

实现流程:

  1. 确定缓存容量(通常取 Runtime.maxMemory() 的 1/8)
  2. 实现淘汰策略(LRU、LFU、FIFO)
  3. 绑定生命周期(随 Activity/Fragment 或 Application)
  4. 读写时加轻量锁或使用并发容器

核心 API 示例(LruCache):

// 计算可用内存并分配缓存
int maxMem = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMem / 8;

LruCache<String, Bitmap> memCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024;
    }
};

// 主线程读取(直接返回,无锁竞争)
Bitmap result = memCache.get("avatar_001");
if (result != null) {
    imageView.setImageBitmap(result);
}

适用场景:高频小数据、UI 资源(图标、缩略图)、配置对象。注意:不要在内存缓存中存放超大 Bitmap 或集合,否则触发 GC 导致主线程卡顿。

2.3 磁盘缓存(Disk Cache)

原理:数据序列化后写入闪存文件系统,依赖 File I/O 和系统 Page Cache。主线程直接读磁盘可能因 I/O 调度产生 10~100ms 毛刺。

实现流程:

  1. 选择缓存目录(context.getCacheDir() 或自定义路径)
  2. 确定最大空间(通常 5~50 MB)
  3. 使用 DiskLruCache 或自研文件索引
  4. 写操作切到子线程,读操作允许主线程但需超时保护

核心 API 示例(DiskLruCache):

// 打开磁盘缓存
DiskLruCache diskCache = DiskLruCache.open(
    cacheDir,          // 缓存目录
    1,                 // app 版本号
    1,                 // 每个 key 对应 value 个数
    50 * 1024 * 1024   // 最大 50MB
);

// 主线程读取(需 try-catch 防止 FileNotFoundException)
String key = hashKeyForDisk("avatar_001");
DiskLruCache.Snapshot snapshot = diskCache.get(key);
if (snapshot != null) {
    InputStream in = snapshot.getInputStream(0);
    Bitmap bitmap = BitmapFactory.decodeStream(in);
    // 直接使用 bitmap
    snapshot.close();
}

适用场景:大图片、离线资源、持久化配置。警告:主线程读磁盘必须设超时(如 20ms),超时后降级到异步回调或默认值。

主线程磁盘读 ANR 风险:闪存损耗导致读延迟从 10ms 飙升到 500ms+,务必在读取路径植入 Looper.getMainLooper().getThread() 监控耗时,超过阈值切换异步。

2.4 网络缓存(Network Cache)

原理:依赖 HTTP 缓存头(Cache-Control、ETag)或客户端本地副本,避免重复请求。主线程直接发起网络请求是 ANR 重灾区。

实现流程:

  1. 配置 OkHttp / Retrofit 内置缓存
  2. 设置缓存目录与最大大小
  3. 服务端配合返回 Cache-Control 头
  4. 主线程只读缓存副本,网络请求强制切子线程

配置示例(OkHttp 缓存):

// 构建 OkHttp 客户端时配置缓存
File cacheDir = new File(context.getCacheDir(), "http_cache");
Cache networkCache = new Cache(cacheDir, 10 * 1024 * 1024); // 10MB

OkHttpClient client = new OkHttpClient.Builder()
        .cache(networkCache)
        .connectTimeout(5, TimeUnit.SECONDS)
        .readTimeout(5, TimeUnit.SECONDS)
        .build();

// 主线程获取缓存(仅读本地磁盘缓存副本)
Request request = new Request.Builder()
        .url("https://example.com/avatar_001.png")
        .build();
Response response = client.newCall(request).execute(); // 切勿在主线程调用!

适用场景:API 响应、远程图片、配置文件。硬规则:主线程永远不执行网络请求,只尝试读取磁盘缓存副本;无缓存时立即返回占位并通过异步加载。

2.5 二级缓存与三级缓存组合策略

单一缓存类型无法同时满足速度、容量和持久性,实际工程中采用分层组合:

组合模式层级读取路径写入策略典型命中率
二级缓存L1 内存 + L2 磁盘读:L1 → L2 → 异步网络写:L1 同步 + L2 异步L1 60% + L2 30%
三级缓存L1 内存 + L2 磁盘 + L3 网络读:L1 → L2 → L3写:L1 同步 + L2 异步 + L3 按需L1 45% + L2 35% + L3 20%

二级缓存实现骨架:

public class L2Cache<K, V> {
    private final LruCache<K, V> memCache;       // L1
    private final DiskLruCache diskCache;         // L2

    public V get(K key) {
        // 1. 查内存缓存(0.1ms)
        V value = memCache.get(key);
        if (value != null) return value;

        // 2. 查磁盘缓存(10~50ms),回填内存
        String diskKey = key.toString();
        DiskLruCache.Snapshot snap = diskCache.get(diskKey);
        if (snap != null) {
            value = deserialize(snap.getInputStream(0));
            memCache.put(key, value);
            return value;
        }

        // 3. 未命中,返回 null 由调用方决定是否异步加载
        return null;
    }
}

三级缓存读取决策:主线程优先命中 L1,L1 miss 后查 L2(磁盘),L2 miss 后通过异步任务查 L3(网络),网络回写 L2 和 L1。三级缓存是 ANR 防护的最终防线:即使磁盘 I/O 抖动,主线程仍可快速返回 L1 旧数据。

图2-1 缓存类型层次架构与主线程读取决策 图2-1 缓存类型层次架构与主线程读取决策 主线程(读取请求) 内存缓存 L1 磁盘缓存 L2 网络缓存 L3 选择原则 • 访问频率:高频→内存 • 数据大小:小→内存 大→磁盘 • 一致性要求:高→穿透缓存 • 生命周期:临时→内存 持久→磁盘 优先读取 (<1ms) L1 miss → 降级 (10~50ms) L2 miss → 异步请求 (>100ms) 回写持久化 预热回写 L1 场景决策 二级缓存 = L1(内存)+ L2(磁盘)|三级缓存 = L1 + L2 + L3(网络),逐级降级保障主线程不卡死

2.6 缓存选择原则与决策矩阵

选择缓存类型时,使用3W1H 决策矩阵量化评估:

<
决策维度权重内存缓存磁盘缓存网络缓存
访问频次高 (0.4)★★★★★★★★★
数据体积