iOS 双层缓存 + 弱网降级架构实践

6 阅读2分钟

前言:双缓存和弱网降级是会让整个app的用户体验得到飞速的提升

一、图片的双缓存机制

一句话总结

  • L1 (LRUCache)缓存已解码的UIImage 命中极速返回
  • L2 (LRUCache+网络)提供原始Data,拿到data后解码并写回L1

L1实现

  • LRU双链表缓存
  • value(for:) 获取图片的唯一标识key获取到 没有就增加到缓存移动到链表表头
let key = "\(url.absoluteString)|\(Int(targetPixelSize.width))x\(Int(targetPixelSize.height))"

if let cached = memoryCache.value(forKey: key) {
    completion(.success(cached))
    return token
}

L2实现

  • 当未命中时候去重请求 将相同的key并发请求共享一个URLSessionDataTask 代码如下
lock.lock()
tokenToKey[token] = key

if var inflight = inFlight[key] {
    inflight.tokens.insert(token)
    inflight.completions.append(completion)
    inFlight[key] = inflight
    lock.unlock()
    return token
}
  • L2(URLCache/网络)取 Data → 空闲调度 → 后台 downsample 解码 → 回写 L1 代码如下
let task = session.dataTask(with: request) { [weak self] data, _, error in
    guard let self else { return }
    if let error {
        self.finish(key: key, result: .failure(error))
        return
    }
    guard let data else {
        self.finish(key: key, result: .failure(NSError(domain: "ImageLoader", code: -1)))
        return
    }
    ...
}

这里的“L2 命中”不是你手动判断,而是由 URLSession + URLCache 自动完成:如果 URLCache 命中,会很快回调 data;否则走网络。

二、弱网降级

一句话总结: 监听网络状态,在网卡的时候也能展示缓存数据或者不会卡流程。做到有内容时不打扰,无内容时给提示。

实现方式

  • 用 NWPathMonitor 持续监听网络路径变化,把结果收敛成线程安全的 isOnline 布尔值。 代码如下
    static let shared = NetworkStateMonitor()

    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkStateMonitor.queue")
    private var _isOnline: Bool = true
    private let lock = NSLock()

    var isOnline: Bool {
        lock.lock()
        defer { lock.unlock() }
        return _isOnline
    }

    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            guard let self else { return }
            let online = (path.status == .satisfied)
            self.lock.lock()
            self._isOnline = online
            self.lock.unlock()
        }
        monitor.start(queue: queue)
    }
}
  • 降级开关: FeatureFlagKey.weakNetworkDegradeEnabled
    .weakNetworkDegradeEnabled: true,
    .diskPostCacheEnabled: true,
    ...
])

降级策略执行者: FeedViewModel

  • 核心点: refresh() 和 loadNextPageIfNeeded() 都会先判断“开关 + 在线状态”。

降级技术细节(按用户可感知行为梳理)

1) 冷启动 / 首次进入:优先读本地缓存(离线可展示)

  • 触发点: FeedViewModel.loadInitial()
  • 条件: diskPostCacheEnabled 开启且 SQLite 有数据
  • 行为:先把缓存 posts 赋值并通知 UI,再继续走一次 refresh() (在线则更新,离线则按降级逻辑处理)
    if featureFlags.bool(.diskPostCacheEnabled) {
        if let cached = try? store.fetchLatest(limit: 50), !cached.isEmpty {
            posts = cached
            onStateChanged?(self)
        }
    }
    await refresh()
}

兜底能力:限流 + 防重复分页

虽然不是“弱网降级”本身,但对弱网体验很关键:

  • RateLimiter.tryAcquire() 抑制短时间重复刷新/分页(弱网下更容易被狂点触发)
func tryAcquire() -> Bool {
        lock.lock()
        defer { lock.unlock() }

        refillIfNeeded(now: CFAbsoluteTimeGetCurrent())
        guard tokens > 0 else { return false }
        tokens -= 1
        return true
    }
  • lastAppendPage 防止重复追加同一页
func refresh() async {
        guard !isLoading else { return }

项目链接