1. 三级缓存架构设计:分层职责、访问顺序与淘汰策略
1.1 三级缓存模型概述
图片三级缓存是解决移动端与桌面端高并发图片加载问题的基础架构,其核心思想是:按照访问速度与存储成本,将图片数据从上到下分层管理。它由三个层次构成:
- 内存缓存(L1):速度最快、容量最小,存储在进程堆或专用内存区域中。
- 磁盘缓存(L2):速度适中、容量较大,以文件或数据库形式持久化到本地存储。
- 网络缓存(L3):速度最慢、容量理论上无限,从远程服务器或CDN获取原始资源。
核心设计原则:每层缓存都充当下一层的前置屏蔽层。如果L1命中,则完全跳过L2和L3;L1未命中则查询L2;L2未命中才发起L3网络请求。这种“由快到慢”的链路称为正查链路。
1.2 各层分层职责与特性
| 缓存层级 | 物理介质 | 典型容量 | 访问延迟 | 持久性 | 典型实现 |
|---|---|---|---|---|---|
| L1 内存缓存 | 堆内存 / DirectBuffer | 数 MB ~ 数百 MB | < 0.1 ms | 应用重启丢失 | LRU LinkedHashMap、LruCache(Android) |
| L2 磁盘缓存 | SSD / HDD 文件系统 | 数 GB ~ 数十 GB | 1 ~ 10 ms | 持久保存 | DiskLruCache、ImageIO文件 |
| L3 网络缓存 | HTTP / CDN / 对象存储 | 不限 | 50 ms ~ 数秒 | 服务端控制 | HTTP Cache-Control、CDN回源 |
1.3 访问顺序与请求流程
一次完整的图片加载遵循严格的三级正查 → 逐级回填路径:
- 发起加载请求:根据图片URL或key,优先查询L1内存缓存。
若命中 → 返回Bitmap/Image对象,流程结束。
若未命中 → 进入L2查询。 - 查询L2磁盘缓存:通过哈希URL在磁盘目录中查找对应的已编码文件。
若命中 → 解码成Bitmap,同时回填L1内存缓存,返回图片。
若未命中 → 进入L3网络加载。 - 发起L3网络请求:通过HTTP/HTTPS下载原始图片数据。
成功 → 将数据依次回填L2磁盘缓存和L1内存缓存,返回Bitmap。
失败 → 抛出异常或返回占位图。
注意:回填操作必须放在异步线程中执行,避免阻塞主线程。特别是网络加载返回的原始字节流,应先在IO线程写入磁盘,再通过解码线程生成Bitmap并推入内存缓存。
1.4 淘汰策略选择
不同层级的缓存具有完全不同的淘汰目标,不能混用同一种策略:
| 缓存层 | 推荐策略 | 淘汰依据 | 实现方式 |
|---|---|---|---|
| L1 内存 | LRU(最近最少使用) | 最近访问时间 | 双向链表 + 哈希表(LinkedHashMap / LruCache) |
| L1 内存 | 可搭配 引用队列 | 对象不可达(WeakReference) | 避免大Bitmap因强引用泄露 |
| L2 磁盘 | LRU + 大小限制 | 文件最后修改时间 + 总目录大小 | DiskLruCache 的 journal 文件 |
| L2 磁盘 | FIFO(先进先出) | 入库时间 | 简单队列,适合冷数据很少被二次访问的场景 |
| L3 网络 | HTTP 标准缓存 | Cache-Control / Expires / ETag | 浏览器端或Proxy自动处理 |
| L3 网络 | CDN 预热/刷新 | 运营规则 / 文件版本 | 主动Push到CDN节点 |
工程选型建议:移动端内存缓存首选
LRU + 大小上限组合,例如Android的LruCache类配合maxSize = maxMemory / 8;磁盘缓存必须设置maxSize(通常取50MB~200MB)以防止磁盘写满;网络层依赖标准HTTP缓存协议,但应用层最好配合图片CDNURL版本号(如md5/时间戳),以便即时清除陈旧缓存。1.5 性能与稳定性优化要点
- 内存抖动控制:L1淘汰时不要直接调用
Bitmap.recycle(),交由GC自然回收,避免触发频繁GC pause。 - 磁盘IO隔离:L2读写必须使用独立线程池,避免与UI线程共享;使用专有文件命名(如
MD5(url).jpg)防止特殊字符路径问题。 - 网络层降级:当L3返回HTTP 304(Not Modified)时,应从L2重新读取本地缓存文件,不再重复回写L1。
- 淘汰策略一致性:L1和L2的LRU时间戳必须统一以最后访问时间为准,避免因回填操作导致淘汰顺序错乱。
1.6 三级缓存架构总览图
1.7 访问优先顺序代码示意
以下为三级缓存分页获取图片时,核心流程的访问优先顺序伪代码:
function loadImage(url):
// 1. 尝试 L1 内存缓存
bitmap = memoryCache.get(url)
if bitmap != null:
return bitmap
// 2. L1 未命中,尝试 L2 磁盘缓存
file = diskCache.get(url)
if file != null:
bitmap = decodeBitmap(file)
memoryCache.put(url, bitmap) // 回填 L1
return bitmap
// 3. L2 未命中,发起 L3 网络请求
byteData = networkFetch(url)
if byteData != null:
diskCache.put(url, byteData) // 回填 L2
bitmap = decodeBitmap(byteData)
memoryCache.put(url, bitmap) // 回填 L1
return bitmap
// 4. 所有层级均失败
return defaultPlaceholder
关键点:回填顺序不能颠倒。必须先写磁盘(L2),再入内存(L1),因为磁盘写入属于IO操作,存在失败风险;如果先入内存再写磁盘时崩溃,会导致下次重启内存丢失后,图片需要重新网络下载。