iOS开发基础38-图片下载与缓存:NSOperationQueue 手动实现与 SDWebImage 原理
图片下载与缓存深度解析:NSOperationQueue 手动实现与 SDWebImage 原理
本文系统梳理 iOS 图片下载与缓存:沙盒目录结构与路径工具类、使用 NSOperationQueue 手动实现多图片下载(内存缓存+磁盘缓存+并发控制+线程安全)、SDWebImage 第三方库的缓存机制与核心 API,以及手动实现与 SDWebImage 的对比。
一、沙盒目录与路径工具
1. 沙盒目录结构
| 目录 | 用途 | 备份 | 清理 |
|---|---|---|---|
| Documents | 应用产生的用户数据(游戏存档、绘图数据) | iCloud 自动备份 | 系统不清理 |
| Library/Caches | 临时缓存(图片缓存、离线数据) | 不备份 | 应用未运行时,磁盘空间不足系统可能清理 |
| tmp | 临时文件(用完即弃) | 不备份 | 重启清空,磁盘不足时清理 |
| Library/Preferences | 用户偏好设置(NSUserDefaults) | iCloud 备份 | 系统不清理 |
注意:网络下载的文件不应保存在 Documents 目录(会被 iCloud 备份,可能导致应用审核不通过),应保存在 Caches 或 tmp。Caches 目录在应用运行时不会被系统清理,但应用未运行且磁盘空间不足时可能被系统清理,因此重要数据仍需考虑重新下载。
2. NSString+Path 路径工具分类
@interface NSString (Path)
- (NSString *)cacheDir; // 拼接 Caches 目录路径
- (NSString *)docDir; // 拼接 Documents 目录路径
- (NSString *)tmpDir; // 拼接 tmp 目录路径
@end
@implementation NSString (Path)
- (NSString *)cacheDir {
NSString *dir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}
- (NSString *)docDir {
NSString *dir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
return [dir stringByAppendingPathComponent:[self lastPathComponent]];
}
- (NSString *)tmpDir {
return [NSTemporaryDirectory() stringByAppendingPathComponent:[self lastPathComponent]];
}
@end
对于 URL 字符串(如
http://example.com/icon.png?version=1),lastPathComponent可能包含查询参数导致文件名异常。生产环境建议对 URL 进行 MD5/SHA 哈希后作为文件名,避免特殊字符和长度问题。
二、NSOperationQueue 手动实现多图片下载
实现目标:UITableView 中每个 cell 显示网络图片,具备内存缓存、磁盘缓存、并发下载、避免重复下载、线程安全。
1. 核心设计
- 内存缓存:
NSCache或NSMutableDictionary,key 为图片 URL,value 为 UIImage。 - 磁盘缓存:图片保存在 Caches 目录,文件名取 URL 的 lastPathComponent(或哈希)。
- 操作缓存:
NSMutableDictionary记录正在下载的 operation,避免重复下载。 - 全局队列:复用一个 NSOperationQueue,设置最大并发数,避免每次创建新队列。
- 线程安全:多线程访问可变字典需加锁(或使用串行队列统一管理)。
2. ViewController 实现
#import "ViewController.h"
#import "XMGApp.h"
#import "NSString+Path.h"
@interface ViewController ()
@property (nonatomic, strong) NSArray *apps;
@property (nonatomic, strong) NSCache *imageCache; // 内存缓存(NSCache 线程安全)
@property (nonatomic, strong) NSMutableDictionary *operations; // 正在下载的操作
@property (nonatomic, strong) NSOperationQueue *downloadQueue; // 全局下载队列
@property (nonatomic, strong) NSLock *lock; // 保护 operations 字典
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = 150;
// 初始化全局下载队列(复用,不每次创建)
self.downloadQueue = [[NSOperationQueue alloc] init];
self.downloadQueue.maxConcurrentOperationCount = 5; // 最大并发数
// NSCache 线程安全,且在内存警告时自动移除对象
self.imageCache = [[NSCache alloc] init];
self.imageCache.countLimit = 100; // 最多缓存 100 张
self.operations = [NSMutableDictionary dictionary];
self.lock = [[NSLock alloc] init];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.apps.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *ID = @"app";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
}
XMGApp *app = self.apps[indexPath.row];
cell.textLabel.text = app.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"下载:%@", app.download];
// 用 tag 标记 cell 对应的 URL,防止复用时图片错位
cell.imageView.tag = indexPath.row;
cell.imageView.image = [UIImage imageNamed:@"placeholder"]; // 占位图
// 1. 查内存缓存
UIImage *image = [self.imageCache objectForKey:app.icon];
if (image) {
cell.imageView.image = image;
return cell;
}
// 2. 查磁盘缓存
NSString *filePath = [app.icon cacheDir];
NSData *diskData = [NSData dataWithContentsOfFile:filePath];
if (diskData) {
UIImage *diskImage = [UIImage imageWithData:diskData];
[self.imageCache setObject:diskImage forKey:app.icon]; // 加入内存缓存
cell.imageView.image = diskImage;
return cell;
}
// 3. 网络下载(避免重复下载)
[self.lock lock];
NSBlockOperation *op = self.operations[app.icon];
[self.lock unlock];
if (!op) {
__weak typeof(self) weakSelf = self;
op = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:app.icon];
NSData *data = [NSData dataWithContentsOfURL:url];
if (!data) {
[weakSelf.lock lock];
[weakSelf.operations removeObjectForKey:app.icon];
[weakSelf.lock unlock];
return;
}
UIImage *downloadedImage = [UIImage imageWithData:data];
// 存内存缓存
if (downloadedImage) {
[weakSelf.imageCache setObject:downloadedImage forKey:app.icon];
}
// 存磁盘缓存
[data writeToFile:filePath atomically:YES];
// 回主线程更新 UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// 通过 tag 找到当前可见的 cell,避免图片错位
UITableViewCell *targetCell = [tableView cellForRowAtIndexPath:indexPath];
if (targetCell && targetCell.imageView.tag == indexPath.row) {
targetCell.imageView.image = downloadedImage;
}
[weakSelf.lock lock];
[weakSelf.operations removeObjectForKey:app.icon];
[weakSelf.lock unlock];
}];
}];
[self.lock lock];
self.operations[app.icon] = op;
[self.lock unlock];
[self.downloadQueue addOperation:op];
}
return cell;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// NSCache 会自动响应内存警告,这里额外清空
[self.imageCache removeAllObjects];
[self.downloadQueue cancelAllOperations];
[self.lock lock];
[self.operations removeAllObjects];
[self.lock unlock];
}
#pragma mark - 懒加载
- (NSArray *)apps {
if (!_apps) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"apps" ofType:@"plist"];
NSArray *arr = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *models = [NSMutableArray arrayWithCapacity:arr.count];
for (NSDictionary *dict in arr) {
[models addObject:[XMGApp appWithDict:dict]];
}
_apps = [models copy];
}
return _apps;
}
@end
3. XMGApp 模型
@interface XMGApp : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *download;
+ (instancetype)appWithDict:(NSDictionary *)dict;
@end
@implementation XMGApp
+ (instancetype)appWithDict:(NSDictionary *)dict {
XMGApp *app = [[self alloc] init];
[app setValuesForKeysWithDictionary:dict];
return app;
}
@end
4. 关键设计点说明
| 问题 | 解决方案 |
|---|---|
| 每次创建新队列 | 复用全局 downloadQueue,设置 maxConcurrentOperationCount |
| 重复下载 | operations 字典记录正在下载的 operation,相同 URL 不重复创建 |
| 线程安全 | NSCache 线程安全;operations 字典用 NSLock 保护 |
| cell 复用图片错位 | 用 tag 标记 cell 对应行,下载完成后校验 tag 再设置图片 |
| 内存警告 | NSCache 自动响应,didReceiveMemoryWarning 中额外清空和取消下载 |
| 缓存查找顺序 | 内存 → 磁盘 → 网络,逐级查找 |
三、SDWebImage 原理与使用
SDWebImage 是 iOS 最常用的图片加载第三方库,封装了下载、缓存、GIF 播放、图片类型识别等功能。
1. 缓存机制
缓存查找顺序:内存缓存 → 磁盘缓存 → 网络下载。
| 缓存层级 | 实现 | 说明 |
|---|---|---|
| 内存缓存 | NSCache |
读取最快,App 启动后有效,内存警告时自动清空 |
| 磁盘缓存 | Library/Caches/default/ |
持久化,默认缓存有效期 7 天,过期自动清理 |
| 网络下载 | NSURLSession |
下载后同时写入内存和磁盘缓存 |
磁盘缓存文件名使用 URL 的 MD5 哈希值,避免特殊字符和长度问题。可通过
SDImageCache手动清理:clearMemory(清内存)、clearDiskOnCompletion:(清磁盘)、cleanDiskWithCompletion:(清过期文件)。
2. 核心功能
- GIF 支持:下载后解析 GIF 所有帧和时长,通过
SDAnimatedImageView播放(旧版用FLAnimatedImage)。 - 图片类型识别:读取图片数据前 8 字节(文件头魔数)判断 PNG/JPEG/GIF/WebP 等类型,不依赖文件扩展名。
- 下载进度:通过
SDWebImageDownloader进度回调获取已下载/总大小。 - 失败重试:
SDWebImageRetryFailed选项,下载失败后自动重试。
3. 基本使用
UIImageView 分类(最常用)
#import <UIImageView+WebCache.h>
// 简单用法:设置 URL 和占位图
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon]
placeholderImage:[UIImage imageNamed:@"placeholder"]];
// 带 options 和完成回调
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:app.icon]
placeholderImage:[UIImage imageNamed:@"placeholder"]
options:SDWebImageRetryFailed | SDWebImageLowPriority
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
if (image) {
NSLog(@"加载成功,缓存类型: %ld", (long)cacheType);
}
}];
常用 options
| 选项 | 说明 |
|---|---|
SDWebImageRetryFailed |
下载失败后重试 |
SDWebImageLowPriority |
低优先级(UIScrollView 滚动时不下载) |
SDWebImageContinueInBackground |
进入后台后继续下载 |
SDWebImageHandleCookies |
处理 cookies |
SDWebImageAllowInvalidSSLCertificates |
允许无效 SSL 证书 |
SDWebImageAvoidAutoSetImage |
下载完成后不自动设置 image,需在 completed 中手动设置 |
SDWebImageManager(手动管理下载)
#import <SDWebImageManager.h>
NSURL *url = [NSURL URLWithString:@"http://example.com/image.jpg"];
[[SDWebImageManager sharedManager] loadImageWithURL:url
options:0
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL *targetURL) {
NSLog(@"下载进度: %zd / %zd", receivedSize, expectedSize);
}
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (image && finished) {
self.imageView.image = image;
}
}];
清除缓存
#import <SDImageCache.h>
// 清内存缓存
[[SDImageCache sharedImageCache] clearMemory];
// 清磁盘缓存(所有文件)
[[SDImageCache sharedImageCache] clearDiskOnCompletion:^{
NSLog(@"磁盘缓存已清除");
}];
// 清过期文件
[[SDImageCache sharedImageCache] cleanDiskWithCompletion:^{
NSLog(@"过期缓存已清除");
}];
4. 手动实现 vs SDWebImage
| 对比项 | 手动 NSOperationQueue | SDWebImage |
|---|---|---|
| 实现复杂度 | 高(需处理缓存、并发、线程安全、复用) | 低(一行代码) |
| 内存缓存 | 自行实现(NSCache/NSDictionary) | 内置 NSCache |
| 磁盘缓存 | 自行实现 | 内置(Caches/default,MD5 文件名) |
| 并发控制 | 自行设置 maxConcurrentOperationCount | 内置管理 |
| 线程安全 | 需自行加锁 | 内置处理 |
| cell 复用防错位 | 需自行处理(tag/校验) | 内置处理(自动取消旧请求) |
| GIF 支持 | 需自行解析播放 | 内置 SDAnimatedImageView |
| 图片类型识别 | 需自行处理 | 内置(文件头识别) |
| 下载进度 | 需自行实现 | 内置 progress 回调 |
| 失败重试 | 需自行实现 | 内置 options |
| 适用场景 | 学习原理、特殊定制 | 生产环境推荐 |
5. 多任务实现方案
| 模块 | 多任务实现方案 |
|---|---|
| 图片网络下载任务 | NSOperation + NSOperationQueue |
| 磁盘缓存读写、清理淘汰 | GCD 串行队列 ioQueue |
| 图片解码(CPU 计算) | GCD 全局并发队列 |
| 回调回到主线程 | dispatch_get_main_queue() GCD 主队列 |
四、Swift 版本对照
SDWebImage(Swift)
import SDWebImage
// UIImageView 分类
cell.imageView.sd_setImage(with: URL(string: app.icon),
placeholderImage: UIImage(named: "placeholder"))
// 带 options 和回调
cell.imageView.sd_setImage(with: URL(string: app.icon),
placeholderImage: UIImage(named: "placeholder"),
options: [.retryFailed, .lowPriority]) { image, error, cacheType, url in
if let image = image {
print("加载成功,缓存类型: \(cacheType)")
}
}
// SDWebImageManager 手动管理
SDWebImageManager.shared.loadImage(with: url, options: [], progress: { received, expected, _ in
print("进度: \(received) / \(expected)")
}, completed: { image, _, _, cacheType, finished, _ in
if let image = image, finished {
self.imageView.image = image
}
})
// 清除缓存
SDImageCache.shared.clearMemory()
SDImageCache.shared.clearDisk()
沙盒路径(Swift)
extension String {
var cacheDir: String {
let dir = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last!
return (dir as NSString).appendingPathComponent((self as NSString).lastPathComponent)
}
}
图片下载(URLSession,Swift 原生)
let url = URL(string: "http://example.com/image.jpg")!
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}.resume()
五、总结
- 沙盒目录:Documents(用户数据,iCloud 备份)、Caches(缓存,不备份,磁盘不足可能清理)、tmp(临时,重启清空)、Preferences(NSUserDefaults)。网络下载文件放 Caches/tmp,不放 Documents。
- 手动实现图片下载:核心流程为 内存缓存 → 磁盘缓存 → 网络下载;需复用全局 NSOperationQueue 并设置最大并发数,用 operations 字典避免重复下载,NSLock 保护多线程访问,tag 标记防止 cell 复用图片错位,NSCache 做内存缓存(线程安全、自动响应内存警告)。
- SDWebImage:封装了内存缓存(NSCache)、磁盘缓存(Caches/default,MD5 文件名,默认 7 天有效期)、网络下载、GIF 播放、图片类型识别(文件头 8 字节)、下载进度、失败重试等功能;通过
sd_setImageWithURL:一行代码实现加载,自动处理 cell 复用和请求取消;生产环境推荐使用。 - 对比:手动实现适合学习原理和特殊定制,SDWebImage 适合生产环境快速开发,功能完善且经过大量验证。
- 缓存查找顺序:内存(最快)→ 磁盘(持久化)→ 网络(最慢,下载后写入两级缓存)。
将来的你会感谢今天如此努力的你!
版权声明:本文为博主原创文章,未经博主允许不得转载。

浙公网安备 33010602011771号