1. 离线缓存需求分析
在移动应用中,网络波动与离线场景是常态。离线缓存并非简单的“存一份数据”,而是一个涉及多维度权衡的系统性设计。本章将分析离线缓存在产品、行为、场景和兜底四个层面的核心诉求,为后续拦截器实现打下设计基础。
一句话理解离线缓存: 在用户感知不到网络变化的前提下,通过本地副本保障内容可访问性,同时平衡数据新鲜度、存储成本和用户体验。
1.1 缓存场景的分类
根据网络状态与用户意图,离线缓存场景可分为四类:
| 场景 | 网络状态 | 用户行为 | 产品期望 |
|---|---|---|---|
| 正常在线浏览 | Wi-Fi / 4G/5G 正常 | 频繁刷新、下拉加载 | 显示最新数据,缓存静默更新 |
| 弱网环境 | 信号差、高延迟 (500ms+) | 等待加载、容易失去耐心 | 优先展示缓存(即使过时),后台发起刷新 |
| 完全离线 | 飞行模式/无网络连接 | 点击进入已访问页面 | 稳定展示缓存内容,明确提示“离线”状态 |
| 网络恢复 | 从离线切回在线 | 继续浏览旧内容 | 静默更新缓存,不打断用户操作 |
1.2 用户行为与产品诉求
用户在不同场景下的行为差异,直接决定了缓存策略的优先级:
- 首次启动: 无缓存可用,必须等待网络请求完成。此时应展示骨架屏或加载状态,避免白屏。
- 重复访问: 用户期望立即看到上次浏览的内容。缓存命中应 < 100ms 返回,而非等待网络。
- 强制刷新: 用户明确要求最新数据。此时应忽略缓存,设置
Cache-Control: no-cache。 - 后台预加载: 在 Wi-Fi 环境下主动 cache 未来可能访问的内容(如新闻列表下一页)。
产品要求清单(MVP 最小可行版本):
1. 应用在 3 秒内必须展示有意义的 UI(优先用缓存填充)。
2. 离线状态下,用户依然可以查看上次已加载的内容。
3. 网络恢复后,自动在后台更新缓存,不弹出 Toast 或 loading。
4. 缓存数据默认存活 30 分钟,超过后需在在线时刷新。
5. 图片/大文件缓存不占用主线程 I/O,异步读写。
1.3 网络不可用的兜底方案
当网络请求失败(IOException 或 HTTP 状态码 5xx/4xx 且无可用重试策略),离线缓存机制必须具备以下兜底能力:
- 抓取本地缓存副本: 优先返回与请求 URL + 参数匹配的缓存文件。
- 展示过期缓存: 允许展示超过 max-age 但尚未被清理的缓存,并在界面上给出视觉提示(如右上角“离线”标签)。
- 阻止空状态闪屏: 即使缓存文件损坏或不存在,也应返回一个可解析的空实体(例如空 JSON
{}或空列表),避免直接崩溃或白屏。 - 静默重试: 在后台启动指数退避重试(例如 5s / 30s / 2min 后自动重试网络请求),成功后自动更新界面。
// 拦截器内兜底逻辑伪代码(非完整实现)
if (networkRequestFailed) {
CacheEntry cached = cacheStore.get(requestKey);
if (cached != null) {
if (cached.isExpired() && isOnline()) {
// 异步刷新缓存,返回旧缓存
asyncRefreshInBackground(request);
return cached.toResponse(StaleWhileRevalidate);
} else {
// 直接返回缓存(过期或未过期)
return cached.toResponse();
}
} else {
// 无缓存:返回 503 或空 body 避免崩溃
return new Response.Builder()
.code(503)
.message("No cache available & offline")
.build();
}
}
1.4 核心场景 SVG 流程
1.5 产品需求与缓存策略对照表
| 产品需求 | 对应缓存策略 | 拦截器实现关键点 |
|---|---|---|
| 3秒内展示内容 | 内存缓存 + 磁盘缓存命中优先 | 拦截器在请求前检查缓存,立即返回 |
| 离线可看已加载内容 | 存储所有请求响应到磁盘 | Interceptor 捕获响应体并序列化存储 |
| 网络恢复静默刷新 | 后台异步请求,更新磁盘缓存 | RxJava/Coroutine 结合拦截器发起重试 |
| 缓存数据最大生存期 | TTL 30分钟,超时后标记过期 | 在缓存元数据中记录时间戳 |
| 图片/文件异步读写 | 使用磁盘 LRU 缓存库 | 拦截器不阻塞主线程,交由线程池 |
1.6 总结与检查点
完成需求分析后,请确认以下问题已明确答案:
- 产品是否接受展示过期缓存?若接受,最长容忍时间是多少?
- 离线缓存是否需要覆盖所有 GET 请求?还是只覆盖核心列表 & 详情页?
- 存储空间上限是多少?超出后执行 FIFO 还是 LRU 淘汰?
- 离线状态下,用户操作(点赞/评论)是否要本地暂存后同步?
常见误区: 不要在一开始就想实现“完美离线缓存”——优先保证“有数据展示”和“不崩溃”,再逐步优化数据新鲜度与存储性能。