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 适合生产环境快速开发,功能完善且经过大量验证。
  • 缓存查找顺序:内存(最快)→ 磁盘(持久化)→ 网络(最慢,下载后写入两级缓存)。

posted @ 2015-08-20 16:51  Mr.陳  阅读(959)  评论(0)    收藏  举报