题目
设计高性能图片缓存组件并解决线程安全与内存抖动问题
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
多级缓存设计,线程安全实现,内存优化,缓存淘汰策略,网络请求优化
快速回答
高性能图片缓存组件的核心设计要点:
- 三级缓存架构:内存缓存(NSCache)+ 磁盘缓存(FileManager)+ 网络下载
- 线程安全:使用串行队列+屏障保证读写安全
- 内存优化:解码后图片存储、自动清理机制、Downsampling技术
- 缓存策略:LRU淘汰算法+Cost-based清理
- 网络优化:请求合并、任务取消、渐进式加载
1. 核心架构设计
三级缓存工作流:
func loadImage(url: URL) -> UIImage? {
// 1. 检查内存缓存
if let memCached = memoryCache.object(forKey: url.absoluteString) {
return memCached
}
// 2. 检查磁盘缓存
if let diskCached = diskCache.getImage(for: url) {
// 异步解码后存入内存
DispatchQueue.global().async {
let decoded = self.decodeImage(diskCached)
self.memoryCache.setObject(decoded, forKey: url.absoluteString)
}
return diskCached
}
// 3. 网络下载
downloadTask = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else { return }
// 后台线程处理
DispatchQueue.global().async {
// 下采样大图
let downsampled = self.downsampleImage(data, to: imageViewSize)
// 磁盘存储
self.diskCache.storeImage(downsampled, for: url)
// 内存缓存
self.memoryCache.setObject(downsampled, forKey: url.absoluteString)
}
}
downloadTask?.resume()
}
2. 线程安全实现
使用GCD屏障保证读写安全:
private let syncQueue = DispatchQueue(label: "com.cache.sync", attributes: .concurrent)
func safeSetImage(_ image: UIImage, for key: String) {
syncQueue.async(flags: .barrier) {
self.memoryCache[key] = image
// 更新LRU链表
self.accessRecorder.moveToHead(for: key)
}
}
3. 内存优化关键技术
- Downsampling:处理大图时使用ImageIO进行下采样
func downsampleImage(data: Data, to size: CGSize) -> UIImage { let options = [kCGImageSourceShouldCacheImmediately: false] guard let source = CGImageSourceCreateWithData(data as CFData, options as CFDictionary) else { return } let maxDimension = max(size.width, size.height) * UIScreen.main.scale let downsampleOptions = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimension ] as CFDictionary guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return } return UIImage(cgImage: cgImage) } - 解码后存储:避免主线程解码卡顿
- Cost-based清理:根据图片像素大小计算cost
4. 缓存淘汰策略
LRU实现方案:
class LRUCache {
private class CacheNode {
var key: String
var value: UIImage
var prev: CacheNode?
var next: CacheNode?
}
private var head: CacheNode?
private var tail: CacheNode?
private var cacheMap = [String: CacheNode]()
func moveToHead(for key: String) {
guard let node = cacheMap[key] else { return }
// ... 链表节点移动操作
}
func removeLast() {
guard let tailNode = tail else { return }
cacheMap.removeValue(forKey: tailNode.key)
// ... 更新链表
}
}
5. 网络请求优化
- 请求合并:相同URL只创建一个任务
- 任务取消:cell复用时的请求取消
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell) { if let task = cell.currentImageTask { task.cancel() } } - 渐进式加载:支持JPEG渐进式显示
6. 常见错误与解决方案
| 错误类型 | 后果 | 解决方案 |
|---|---|---|
| 主线程解码 | 界面卡顿 | 强制在后台线程解码 |
| 未计算内存cost | OOM崩溃 | 根据像素大小计算cost |
| 线程竞争 | 随机崩溃 | 屏障队列保护共享资源 |
| 未处理复用 | 图片错乱 | 绑定请求与cell实例 |
7. 扩展知识
- 响应式内存警告:监听
UIApplication.didReceiveMemoryWarningNotification清空缓存 - 磁盘缓存加密:使用SQLCipher加密敏感图片
- WebP支持:集成libwebp解码器
- Prefetching:结合
UITableViewDataSourcePrefetching预加载