前言:双缓存和弱网降级是会让整个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 }