题目
设计一个图片下载和缓存机制
信息
- 类型:问答
- 难度:⭐⭐
考点
网络请求, 缓存策略, 多线程管理, 内存管理
快速回答
实现一个高效的图片下载缓存机制需要:
- 使用
NSCache实现内存缓存,响应内存警告 - 采用
NSOperationQueue管理下载任务,控制并发量 - 磁盘缓存使用
FileManager存储到Caches目录 - 下载过程使用
URLSessionDataTask支持渐进式加载 - 处理重复请求和线程安全问题
核心设计原理
高效图片缓存机制需分层实现:
1. 内存缓存:使用NSCache存储解码后的图片,比Dictionary更优(自动释放内存)
2. 磁盘缓存:将图片数据写入Caches目录,使用FileManager管理
3. 下载队列:通过OperationQueue控制并发,避免线程爆炸
代码实现示例
class ImageLoader {
static let shared = ImageLoader()
// 内存缓存
private let memoryCache = NSCache<NSString, UIImage>()
// 下载队列
private let downloadQueue = OperationQueue()
// 追踪进行中的任务
private var runningRequests = [UUID: URLSessionDataTask]()
init() {
downloadQueue.maxConcurrentOperationCount = 5
// 内存警告处理
NotificationCenter.default.addObserver(
self,
selector: #selector(clearMemoryCache),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
}
@objc func clearMemoryCache() {
memoryCache.removeAllObjects()
}
func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) -> UUID? {
// 1. 检查内存缓存
if let image = memoryCache.object(forKey: url.absoluteString as NSString) {
completion(image)
return nil
}
// 2. 检查磁盘缓存(伪代码)
if let diskImage = loadFromDisk(url: url) {
memoryCache.setObject(diskImage, forKey: url.absoluteString as NSString)
completion(diskImage)
return nil
}
// 3. 创建下载任务
let uuid = UUID()
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
defer { self?.runningRequests.removeValue(forKey: uuid) }
guard let data = data, error == nil,
let image = UIImage(data: data) else {
completion(nil)
return
}
// 缓存到内存
self?.memoryCache.setObject(image, forKey: url.absoluteString as NSString)
// 存储到磁盘(伪代码)
self?.saveToDisk(image: data, url: url)
DispatchQueue.main.async { completion(image) }
}
task.resume()
// 记录进行中的任务
runningRequests[uuid] = task
return uuid
}
func cancelLoad(_ uuid: UUID) {
runningRequests[uuid]?.cancel()
runningRequests.removeValue(forKey: uuid)
}
}最佳实践
- 内存优化:使用
NSCache而非Dictionary,它会在内存紧张时自动释放 - 磁盘存储:
- 存储到
Caches目录(系统可自动清理) - 使用
FileManager创建子目录分类存储 - 对文件名做
MD5哈希避免非法字符
- 存储到
- 线程安全:
- 使用
@synchronized或串行队列保护runningRequests - 所有缓存操作在后台线程执行
- 使用
常见错误
- 内存泄漏:未使用
[weak self]导致循环引用 - 线程阻塞:在主线程解码大图导致卡顿(应在后台线程解码)
- 缓存穿透:相同URL重复发起下载,需用
runningRequests去重 - 磁盘误存:将缓存写入
Documents目录导致无法被系统清理
扩展知识
- 渐进式加载:使用
URLSessionDataTask的didReceiveData逐步显示图片 - 缓存淘汰策略:LRU(最近最少使用)算法实现磁盘缓存清理
- 第三方库对比:
- SDWebImage:成熟稳定,支持动图
- Kingfisher:Swift编写,语法更现代
- Nuke:高性能,低内存占用