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 且无可用重试策略),离线缓存机制必须具备以下兜底能力:

  1. 抓取本地缓存副本: 优先返回与请求 URL + 参数匹配的缓存文件。
  2. 展示过期缓存: 允许展示超过 max-age 但尚未被清理的缓存,并在界面上给出视觉提示(如右上角“离线”标签)。
  3. 阻止空状态闪屏: 即使缓存文件损坏或不存在,也应返回一个可解析的空实体(例如空 JSON {} 或空列表),避免直接崩溃或白屏。
  4. 静默重试: 在后台启动指数退避重试(例如 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 淘汰?
  • 离线状态下,用户操作(点赞/评论)是否要本地暂存后同步?
常见误区: 不要在一开始就想实现“完美离线缓存”——优先保证“有数据展示”和“不崩溃”,再逐步优化数据新鲜度与存储性能。