IOS源码解析:SDWeblmage (上)

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、系统提供的NSURLSession的使用
    • 1、简介
    • 2、Get请求
    • 3、发送POST请求
    • 4、下载文件
    • 5、监听具体请求状态的代理
  • 二、认识SDWeblmage框架
    • 1、简介
    • 2、使用方法
    • 3、目录结构
    • 4、sd_setImageWithURL:核心方法
    • 5、拓展
  • 四、SDWebImageDownloader 管理所有的下载任务
    • 1、属性与枚举
    • 2、Lifecycle
    • 3、Setter和Getter
    • 4、核心方法:下载图片
  • 五、SDImageDownloaderOperation 具体的下载任务
    • 1、属性与方法
    • 2、回调块
    • 3、启动下载任务
    • 4、设置取消与完成下载任务
    • 5、NSURLSessionDataDelegate 代理方法
    • 6、NSURLSessionTaskDelegate 代理方法
  • Demo
  • 参考文献

一、系统提供的NSURLSession的使用

1、简介

NSURLSession工作在OSI 七层模型的会话层。会话层之下的所有工作,系统都已经帮我们做好了,所以这里的Session也可以理解为会话。NSURLSession提供了丰富的类来支持GET/POST请求、支持后台下载和上传,可将文件直接下载到磁盘的沙盒中。

为了方便使用,NSURLSession提供了一个单例的方法来获取一个全局共享的session对象,接下来通过这个session对象构造了一个请求任务,即NSURLSessionDataTask类的对象。这个类是NSURLSessionTask的子类,主要用于进行一些比较简短数据的获取,通常用于发送GET/POST请求。默认发起GET请求,如果需要发起POST请求需要额外的操作。创建的任务默认是挂起状态的,所以为了启动网络请求,调用其resume方法即可开始执行请求。当任务完成时就会执行上述回调块,当然也可以使用代理的方式监听网络请求。这样看来它的使用真的很方便,并且默认会自动开启多线程异步执行。下面栗子的回调块中输出了当前线程可以看出并不是主线程,所以在回调中如果要进行UI的更新操作需要放到主线程中执行。

NSURLSession也提供了丰富的代理来监听具体请求的状态。我们无法为全局共享的NSURLSession对象设置代理,也就不能监听其网络请求。原因很简单,委托对象只有一个,而全局共享的单例对象可能有很多类都在使用。所以只能自己创建一个NSURLSession对象并在初始化方法中指定其委托对象。

NSURLSessionTask类似抽象类不提供网络请求的功能,具体实现由其子类实现。上面的栗子使用的就是NSURLSessionDataTask主要用来获取一些简短的数据,如发起GET/POST请求。NSURLSessionDownloadTask用于下载文件,它提供了很多功能,默认支持将文件直接下载至磁盘沙盒中,这样可以避免占用过多内存的问题。NSURLSessionUploadTask用于上传文件。NSURLSessionStreamTask提供了以流的形式读写TCP/IP流的功能,可以实现异步读写的功能。前面三个类使用的比较频繁,在SDWebImage中用于下载图片的具体任务是交由NSURLSessionDataTask完成。由于缓存策略的问题,图片一般都较小,可能不需要将图片保存至磁盘,所以也就不需要使用NSURLSessionDownloadTask


2、Get请求

NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"https://www.douban.com/j/app/radio/channels"];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"数据:%@,错误:%@,线程:%@", data, error, [NSThread currentThread]);
    NSLog(@"回应:%@", response);
}];
[task resume];

输出结果为:

2021-02-02 16:35:05.560444+0800 SDWebImageSourceCodeAnalysis[77552:10355246] 数据:{length = 3603, bytes = 0x7b226368 616e6e65 6c73223a 5b7b226e ... 6e223a22 227d5d7d },错误:(null),线程:<NSThread: 0x60000300d800>{number = 6, name = (null)}
2021-02-02 16:35:05.560672+0800 SDWebImageSourceCodeAnalysis[77552:10355246] 回应:<NSHTTPURLResponse: 0x60000250e1c0> { URL: https://www.douban.com/j/app/radio/channels } { Status Code: 200, Headers {
    "Cache-Control" =     (
        "must-revalidate, no-cache, private"
    );
...

3、发送POST请求

// 创建NSURL的请求路径URL
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8080/login"];

// 创建一个可变的request对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

// 修改请求方式为POST方法,默认是GET方法
[request setHTTPMethod:@"POST"];

// 设置请求体,即添加post请求数据
[request setHTTPBody:[@"username=xiejiapei&password=Sgahd" dataUsingEncoding:NSUTF8StringEncoding]];

// 使用单例的全局共享的session对象
NSURLSession *session = [NSURLSession sharedSession];

// 使用上述request构造一个任务对象
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    // 请求完成后会执行回调块,可以根据服务端返回的数据转换为JSON数据或者HTML等格式
    NSLog(@"数据:%@,错误:%@,线程:%@", data, error, [NSThread currentThread]);
    NSLog(@"回应:%@", response);
}];

// 启动任务
[task resume];

4、下载文件

// 创建文件地址URL
NSURL *url = [NSURL URLWithString:@"http://mirrors.hust.edu.cn/apache/tomcat/tomcat-9/v9.0.1/bin/apache-tomcat-9.0.1.tar.gz"];

// 获取单例全局共享的session对象
NSURLSession *session = [NSURLSession sharedSession];

// 创建一个下载任务
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
   // 这个location就是下载到磁盘的位置,默认是在沙盒tmp文件夹中
    NSLog(@"下载到磁盘的位置:%@", location);
    
    // tmp文件夹在关闭app后会自动删除,有需要可以使用NSFileManager将该文件转移到沙盒其他目录下
    NSFileManager *fileManager = [NSFileManager defaultManager];
    [fileManager copyItemAtPath:location.path toPath:@"..." error:nil];
}];
// 启动任务
[downloadTask resume];

输出结果为:

2021-02-02 16:41:17.144667+0800 SDWebImageSourceCodeAnalysis[77622:10362968] 下载到磁盘的位置:file:///Users/xiejiapei/Library/Developer/CoreSimulator/Devices/5BC32A40-EDB6-4954-A93D-DE1741EFFB53/data/Containers/Data/Application/1CEA5DE3-725D-416E-A168-91E0F5F1F2DE/tmp/CFNetworkDownload_QGRKB3.tmp

5、监听具体请求状态的代理

a、设置代理和代理方法执行队列

Foundation框架提供了三种NSURLSession的运行模式,即三种NSURLSessionConfiguration会话配置。defaultSessionConfiguration默认Session运行模式,使用该配置默认使用磁盘缓存网络请求相关数据如cookie等信息。ephemeralSessionConfiguration临时Session运行模式,不缓存网络请求的相关数据到磁盘,只会放到内存中使用。backgroundSessionConfiguration后台Session运行模式,如果需要实现在后台继续下载或上传文件时需要使用该会话配置,需要配置一个唯一的字符串作为区分。同时,NSURLSessionConfiguration还可以配置一些其他信息,如缓存策略、超时时间、是否允许蜂窝网络访问等信息。

@interface ViewController ()<NSURLSessionDelegate>

// 创建一个代理方法执行的队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 设置队列的名称
queue.name = @"MyDelegateQueue";

// 创建一个session,运行在默认模式下
// 设置代理和代理方法执行队列
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:queue];

// 创建一个任务
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
// 启动任务
[task resume];

b、收到服务端响应时执行,一次请求只会执行一次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    NSLog(@"Receive Response %@ %@ %@", response, [NSThread currentThread], [NSOperationQueue currentQueue]);
    
    // 如果要实现这个代理方法一定要执行这个回调块,如果不执行这个回调块默认就会取消任务,后面就不会从服务器获取数据了,后面的回调方法都不会再执行
    if (completionHandler)
    {
        completionHandler(NSURLSessionResponseAllow);
    }
}

c、从服务端收到数据,一次请求中可能执行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    NSLog(@"Receive Data %@",  [NSOperationQueue currentQueue]);
}

d、任务完成后的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
{
    NSLog(@"Complete %@ %@", error, [NSOperationQueue currentQueue]);
}

e、输出结果

从输出结果看代理方法都是在子线程中执行,执行的队列也是我们创建的队列,如果需要在主线程中执行代理就将代理队列设置为主队列即可。

2021-02-02 16:55:08.618204+0800 SDWebImageSourceCodeAnalysis[77799:10379003] Receive Response <NSHTTPURLResponse: 0x6000003a62e0> { URL: http://www.baidu.com/ } { Status Code: 200, Headers {
    "Content-Encoding" =     (
        gzip
    );
    "Content-Length" =     (
        1108
    );
    "Content-Type" =     (
        "text/html"
    );
    Date =     (
        "Tue, 02 Feb 2021 08:55:08 GMT"
    );
    Server =     (
        bfe
    );
} } <NSThread: 0x6000016c1240>{number = 4, name = (null)} <NSOperationQueue: 0x7f878ad0b430>{name = 'MyDelegateQueue'}
2021-02-02 16:55:08.618370+0800 SDWebImageSourceCodeAnalysis[77799:10379003] Receive Data <NSOperationQueue: 0x7f878ad0b430>{name = 'MyDelegateQueue'}
2021-02-02 16:55:08.618586+0800 SDWebImageSourceCodeAnalysis[77799:10379003] Complete (null) <NSOperationQueue: 0x7f878ad0b430>{name = 'MyDelegateQueue'}

二、认识SDWeblmage框架

1、简介

a、设计目的

SDWebImage提供了 UIImageViewUIButtonMKAnnotationView的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。这样开发者就无须花太多精力在图片下载细节上,专心处理业务逻辑。


b、特性
  • 异步下载图片,不阻塞主线程
  • 异步缓存(内存+磁盘),自动管理缓存有效性
  • 在后台进行图片解压缩
  • 同一个 URL 不会重复下载,并且自动识别无效 URL,不会反复重试
  • 支持多种图片格式,并支持动图(GIF

2、使用方法

a、sd_setImageWithURL

block中得到图片下载进度和图片加载完成(下载完成或者读取缓存)的回调,如果你在图片加载完成前取消了请求操作,就不会收到成功或失败的回调。

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                ... completion code here ...
                             }];

b、SDWebImageDownloader:异步下载图片
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
[downloader downloadImageWithURL:imageURL
                         options:0
                        progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                            // progression tracking code
                        }
                       completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                            if (image && finished)
                            {
                                // do something with image
                            }
                        }];

c、SDImageCache:支持内存缓存和磁盘缓存
❶ 添加缓存的方法
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
❷ 默认情况下,图片数据会同时缓存到内存和磁盘中,如果你想只要内存缓存的话,可以使用下面的方法
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey toDisk:NO];
❸ 读取缓存的方法
// 图片缓存的 key 是唯一的,通常就是图片的 absolute URL
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
    // image is not nil if image was found
}];
❹ 自定义缓存 key

有时候,一张图片的 URL 中的一部分可能是动态变化的(比如获取权限上的限制),所以我们只需要把 URL 中不变的部分作为缓存用的key

SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) {
    url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
    return [url absoluteString];
};

d、SDWebImageManager:将图片下载和图片缓存组合
SDWebImageManager *manager = [SDWebImageManager sharedManager];
[manager loadImageWithURL:imageURL
                  options:0
                 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                        // progression tracking code
                 }
                 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                    if (image)
                    {
                        // do something with image
                    }
                 }];

3、目录结构

Downloader:下载
  • SDWebImageDownloader:专门用来下载图片的,跟缓存没有关系
  • SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的
Cache:缓存
  • SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程
Utils:工具类
  • SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来
  • SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码
  • SDWebImagePrefetcher:预下载图片,方便后续使用
Categories:分类
  • UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
  • UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用
  • UIImageView+HighlightedWebCache:也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片
  • UIButton+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中
  • MKAnnotationView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 MKAnnotationView 的方法中
  • NSData+ImageContentType:用于获取图片数据的格式(JPEGPNG等)
  • UIImage+GIF:用于加载 GIF 动图
  • UIImage+MultiFormat:将不同格式的二进制数据转成 UIImage 对象
  • UIImage+WebP:用于解码并加载 WebP 图片
Other:其他
  • SDWebImageOperation(协议)
  • SDWebImageCompat(宏定义、常量、通用函数)

4、sd_setImageWithURL:核心方法

a、外界调用
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:[_objects objectAtIndex:indexPath.row]] placeholderImage:[UIImage imageNamed:@"placeholder"] options:indexPath.row == 0 ? SDWebImageRefreshCached : 0];

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
{
    ...
}
b、取消当前正在进行的加载任务
[self sd_cancelCurrentImageLoad];
c、通过关联对象将 url 作为成员变量存起来
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
d、设置占位图
if (!(options & SDWebImageDelayPlaceholder))
{
    dispatch_main_async_safe(^{
        self.image = placeholder;
    });
}
e、URL 为空时,直接回调 completedBlock,返回错误信息
dispatch_main_async_safe(^{
    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
    if (completedBlock)
    {
        completedBlock(nil, error, SDImageCacheTypeNone, url);
    }
});
f、如果 URL 不为空
❶ 调用 SDWebImageManager 的 downloadImage()方法开始加载图片,返回SDWebImageOperation
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
{
    ...
}
❷ 如果不需要自动设置 image,直接 return
dispatch_main_sync_safe(^{
    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
    {
        completedBlock(image, error, cacheType, url);
        return;
    }
    ...
});
❸ 图片下载成功,设置 image
wself.image = image;
[wself setNeedsLayout];
❹ 图片下载失败,设置 placeholder
if ((options & SDWebImageDelayPlaceholder))
{
    wself.image = placeholder;
    [wself setNeedsLayout];
}
❺ 回调 completedBlock
if (completedBlock && finished)
{
    completedBlock(image, error, cacheType, url);
}
g、借助 UIView+WebCacheOperation 将获得的 operation 保存到成员变量中去
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

5、拓展

后台下载

使用-[UIApplication beginBackgroundTaskWithExpirationHandler:]方法使 app 退到后台时还能继续执行任务,不再执行后台任务时,需要调用 -[UIApplication endBackgroundTask:] 方法标记后台任务结束。

文件的缓存有效期及最大缓存空间大小

默认有效期:

maxCacheAge = 60 * 60 * 24 * 7; // 1 week

默认最大缓存空间:

maxCacheSize = <#unlimited#>
MKAnnotationView

MKAnnotationView 是属于 MapKit 框架的一个类,继承自UIView,是用来展示地图上的annotation 信息的,它有一个用来设置图片的属性 image

假如自己来实现一个图片下载工具,该怎么写?

图片读写:以图片URL的单向Hash值作为Key
淘汰策略:以队列先进先出的方式淘汰,LRU算法(如30分钟之内是否使用过)
磁盘设计:存储方式、大小限制(如100MB )、淘汰策略(如某图片存储时间距今已超过7天)
网络设计:图片请求最大并发量、请求超时策略、请求优先级
图片解码:对于不同格式的图片,解码采用什么方式来做? 在哪个阶段做图片解码处理?(磁盘读取后网络请求返回后)


四、SDWebImageDownloader 管理所有的下载任务

  • 如何实现异步下载,也就是多张图片同时下载?
  • 如何处理同一张图片(同一个 URL)多次下载的情况?

1、属性与方法

a、辅助变量
声明通知的全局变量名
NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
下载选项
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0, //低优先级
    SDWebImageDownloaderProgressiveDownload = 1 << 1, //带有下载进度
    SDWebImageDownloaderUseNSURLCache = 1 << 2, //使用缓存
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, //忽略缓存响应
    SDWebImageDownloaderContinueInBackground = 1 << 4, //支持后台下载
    SDWebImageDownloaderHandleCookies = 1 << 5, //使用Cookies
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, //允许验证SSL
   SDWebImageDownloaderHighPriority = 1 << 7, //高优先级
};

下载选项枚举使用了位运算。通过“与”运算符,可以判断是否设置了某个枚举选项,因为每个枚举选择项中只有一位是1,其余位都是 0,所以只有参与运算的另一个二进制值在同样的位置上也为 1,与运算的结果才不会为 0。

0101 (相当于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
& 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache)
= 0100 (> 0,也就意味着 option 参数中设置了 SDWebImageDownloaderUseNSURLCache)
下载任务执行顺序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) 
{
    SDWebImageDownloaderFIFOExecutionOrder, //执行顺序为先进先出
    SDWebImageDownloaderLIFOExecutionOrder //执行顺序为后进先出
};
回调块
// 进度回调块
typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock;
// 下载完成的回调块
typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock;

b、属性
公开属性
@property (assign, nonatomic) BOOL shouldDecompressImages; //下载完成后是否需要解压缩图片,默认为 YES
@property (assign, nonatomic) NSInteger maxConcurrentDownloads; //支持的最大同时下载图片的数量,其实就是NSOperationQueue支持的最大并发数
@property (readonly, nonatomic) NSUInteger currentDownloadCount; //当前正在下载图片的数量,其实就是NSOperationQueue的operationCount,即正在执行下载任务的operation的数量
@property (assign, nonatomic) NSTimeInterval downloadTimeout; //下载时连接服务器的超时时间,默认15s
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //执行下载任务的顺序,FIFO或LIFO

@property (strong, nonatomic) NSURLCredential //默认的URL credential*urlCredential;
@property (strong, nonatomic) NSString *username; //用户名,有些图片下载的时候需要做用户认证
@property (strong, nonatomic) NSString *password; //密码
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter; //过滤http首部的回调块
私有属性
@property (strong, nonatomic) NSOperationQueue *downloadQueue; //图片下载任务是放在这个 NSOperationQueue 任务队列中来管理的
@property (weak, nonatomic) NSOperation *lastAddedOperation; //最近一次添加进队列的operation,主要用于LIFO时设置依赖
@property (assign, nonatomic) Class operationClass; //默认是SDWebImageDownloaderOperation
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; //可变字典,存储http首部
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; //一个GCD的队列
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; //图片下载的回调 block 都是存储在这个属性中,该属性是一个字典,key 是图片的 URL,value 是一个数组,包含每个图片的多组回调信息

c、接口方法
// 类方法,获取全局共享的单例对象
+ (SDWebImageDownloader *)sharedDownloader;

// 为http首部设置值
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;

// 返回http首部的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field;

// 设置下载类的Class,默认使用SDWebImageDownloaderOperation,开发者可以实现相关协议进行自定义
- (void)setOperationClass:(Class)operationClass;

// 设置下载队列NSOperationQueue挂起          
- (void)setSuspended:(BOOL)suspended;

/*
下载url对应的图片
设置下载配置选项、进度回调块、下载完成回调块
返回一个token,用于取消对应的下载任务
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

2、Lifecycle

a、initialize:类方法,类加载的时候执行
+ (void)initialize
{
    ...
}
❶ 如果导入了SDNetworkActivityIndicator文件,就会展示一个小菊花

为了让 SDNetworkActivityIndicator 文件可以不用导入项目中来(如果不要的话),这里使用了 runtime 的方式来实现动态创建类以及调用方法。

if (NSClassFromString(@"SDNetworkActivityIndicator"))
{
    id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
    ...
}
❷ 删除加载通知后重新添加加载通知,防止重复添加出现异常

这个方法中主要是通过注册通知让小菊花监听下载事件来显示和隐藏状态栏上的网络活动指示器。

[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                         selector:NSSelectorFromString(@"startActivity")
                                             name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
                                         selector:NSSelectorFromString(@"stopActivity")
                                             name:SDWebImageDownloadStopNotification object:nil];

b、sharedDownloader:类方法,返回单例对象
+ (SDWebImageDownloader *)sharedDownloader 
{
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

c、init:初始化方法
- (id)init
{
    if ((self = [super init]))
    {
        ...
    }
    return self;
}
❶ 默认使用SDWebImageDownloaderOperation作为下载任务Operation
_operationClass = [SDWebImageDownloaderOperation class];
❷ 设置需要压缩下载的图片
_shouldDecompressImages = YES;
❸ 设置下载 operation 的默认执行顺序为FIFO(先进先出还是先进后出)
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
❹ 初始化 _downloadQueue(下载队列)并设置最大并发数为6,即同时最多可以下载6张图片
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
❺ 初始化 _URLCallbacks(下载回调 block 的容器)
_URLCallbacks = [NSMutableDictionary new];
❻ 设置下载webp格式图片的http首部
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
❼ 初始化 _barrierQueue(创建一个GCD并发队列)
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
❽ 设置默认下载超时时长 15s
_downloadTimeout = 15.0;

d、dealloc:析构函数
- (void)dealloc
{
    // NSOperationQueue取消所有的下载操作
    [self.downloadQueue cancelAllOperations];
    // 释放GCD队列
    SDDispatchQueueRelease(_barrierQueue);
}

3、Setter和Getter

a、http首部
获取http首部的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field
{
    return self.HTTPHeaders[field];
}
为http首部设置键值对
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field
{
    if (value)
    {
        self.HTTPHeaders[field] = value;
    }
    else
    {
        [self.HTTPHeaders removeObjectForKey:field];
    }
}

b、下载图片的数量
设置最大同时下载图片的数量,即NSOperationQueue最大并发数
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads
{
    _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
获取最大同时下载图片的数量
- (NSInteger)maxConcurrentDownloads
{
    return _downloadQueue.maxConcurrentOperationCount;
}
当前正在下载图片数量,即NSOperationQueue中正在执行的operation数量
- (NSUInteger)currentDownloadCount
{
    return _downloadQueue.operationCount;
}
设置operation的Class类对象
- (void)setOperationClass:(Class)operationClass
{
    _operationClass = operationClass ?: [SDWebImageDownloaderOperation class];
}

c、设置是否挂起下载队列
- (void)setSuspended:(BOOL)suspended
{
    [self.downloadQueue setSuspended:suspended];
}

4、核心方法:下载图片

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
{
    __block SDWebImageDownloaderOperation *operation;
    // block中为了防止引用循环和空指针,先weak后strong
    __weak __typeof(self)wself = self;
    ...
    // 返回 createCallback 中创建的 operation(SDWebImageDownloaderOperation)
    return operation;
}
a、直接调用另一个方法:addProgressCallback
// 把入参 url、progressBlock 和 completedBlock 传进该方法创建一个SDWebImageDownloaderOperation类的对象,并在第一次下载该 URL 时回调 createCallback。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ...
}];
❶ 设置超时时间
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0)
{
    timeoutInterval = 15.0;
}
❷ 创建一个可变的request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
❸ 设置cookie的处理策略
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
❹ 过滤http首部
if (wself.headersFilter)
{
    request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else
{
    request.allHTTPHeaderFields = wself.HTTPHeaders;
}
❺ 传入网络请求和下载选项配置创建DownloaderOperation类的对象
operation = [[wself.operationClass alloc] initWithRequest:request options:options
❻ 设置下载完成后是否需要解压缩
operation.shouldDecompressImages = wself.shouldDecompressImages;
❼ 如果设置了 username 和 password,就给 operation 的下载请求设置一个 NSURLCredential 认证凭证
if (wself.username && wself.password)
{
    operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
❽ 设置 operation 的队列优先级
if (options & SDWebImageDownloaderHighPriority)
{
    operation.queuePriority = NSOperationQueuePriorityHigh;
}
else if (options & SDWebImageDownloaderLowPriority)
{
    operation.queuePriority = NSOperationQueuePriorityLow;
}
❾ 将 operation 加入到队列 downloadQueue 中,队列(NSOperationQueue)会自动管理 operation 的执行
// 向队列中添加创建的下载任务,之后这个operation就会被线程调度来执行其start方法
[wself.downloadQueue addOperation:operation];
❿ 如果 operation 执行顺序是先进后出,就设置 operation 依赖关系(先加入的依赖于后加入的),并记录最后一个 operation(lastAddedOperation)
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder)
{
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

b、progressBlock 回调处理
❶ 这个 block 有两个回调参数——接收到的数据大小和预计数据大小
progress:^(NSInteger receivedSize, NSInteger expectedSize)
❷ 这里用了 weak-strong dance

首先使用 strongSelf 强引用 weakSelf,目的是为了保住 self 不被释放。然后检查 self 是否已经被释放(这里为什么先“保活”后“判空”呢?因为如果先判空的话,有可能判空后 self 就被释放了)。

SDWebImageDownloader *sself = wself;
if (!sself) return;
❸ 取出 url 对应的回调 block 数组。这里取的时候有些讲究,考虑了多线程问题,而且取的是 copy 的内容。
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
    callbacksForURL = [sself.URLCallbacks[url] copy];
});
❹ 遍历数组,从每个元素(字典)中取出 progressBlock 进行回调
for (NSDictionary *callbacks in callbacksForURL)
{
    SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
    if (callback) callback(receivedSize, expectedSize);
}

c、completedBlock 回调处理
❶ 这个 block 有四个回调参数——图片 UIImage,图片数据 NSData,错误 NSError,是否结束 isFinished
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) 
❷ 同样,这里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
❸ 接着,取出 url 对应的回调 block 数组

如果结束了(isFinished),就移除 url 对应的回调 block 数组。注意移除的时候也要考虑多线程问题。

__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
    callbacksForURL = [sself.URLCallbacks[url] copy];

    if (finished)
    {
        [sself.URLCallbacks removeObjectForKey:url];
    }
});
❹ 遍历数组,从每个元素(字典)中取出 completedBlock`进行回调
for (NSDictionary *callbacks in callbacksForURL)
{
    SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
    if (callback) callback(image, data, error, finished);
}

d、 cancelBlock 回调处理
❶ 同样,这里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
❷ 然后移除 url 对应的所有回调 block
dispatch_barrier_async(sself.barrierQueue, ^{
    [sself.URLCallbacks removeObjectForKey:url];
});

e、前面download方法调用的方法,返回一个token
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback
{
    ...
}
❶ 判断 url 是否为 nil,如果为 nil 则直接回调 completedBlock 下载完成回调块,返回失败的结果,然后 return
if (url == nil)
{
    if (completedBlock != nil)
    {
        completedBlock(nil, nil, nil, NO);
    }
    return;
}
❷ 使用 dispatch_barrier_sync 函数来保证同一时间只有一个线程能对 URLCallbacks 进行操作

这里有个细节需要注意,因为可能同时下载多张图片,所以就可能出现多个线程同时访问 URLCallbacks 属性的情况。为了保证线程安全,所以这里使用了 dispatch_barrier_sync 来分步执行添加到 barrierQueue 中的任务,这样就能保证同一时间只有一个线程能对URLCallbacks 进行操作。

dispatch_barrier_sync(self.barrierQueue, ^{
    ...
});
❸ 如果没有取到,也就意味着这个 url 是第一次下载,那就初始化一个 callBacksForURL 放到属性 URLCallbacks 中
// 这是一个数组,因为一个 url 可能不止在一个地方下载
BOOL first = NO;
if (!self.URLCallbacks[url])
{
    self.URLCallbacks[url] = [NSMutableArray new];
    first = YES;
}
❹ 从属性 URLCallbacks(一个字典) 中取出对应 url 的 callBacksForURL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
❺ 往数组 callBacksForURL 中添加包装有 callbacks(progressBlock 和 completedBlock)的字典
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
❻ 更新 URLCallbacks 存储的对应 url 的 callBacksForURL
self.URLCallbacks[url] = callbacksForURL;
❼ 处理同一个 URL 的多次下载请求。如果这个URL是第一次被下载,就要回调 createCallback,createCallback 主要做的就是创建并开启下载任务
if (first)
{
    createCallback();
}

URLCallbacks 属性是一个 NSMutableDictionary 对象,key 是图片的 URLvalue 是一个数组,包含每个图片的多组回调信息 。用 JSON 格式表示的话,就是下面这种形式。

{
    "callbacksForUrl1": [
        {
            "kProgressCallbackKey": "progressCallback1_1",
            "kCompletedCallbackKey": "completedCallback1_1"
        },
        {
            "kProgressCallbackKey": "progressCallback1_2",
            "kCompletedCallbackKey": "completedCallback1_2"
        }
    ],
    "callbacksForUrl2": [
        {
            "kProgressCallbackKey": "progressCallback2_1",
            "kCompletedCallbackKey": "completedCallback2_1"
        },
        {
            "kProgressCallbackKey": "progressCallback2_2",
            "kCompletedCallbackKey": "completedCallback2_2"
        }
    ]
}

五、SDImageDownloaderOperation 具体的下载任务

该类继承自NSOperation,实现了相关的自定义操作,所以上层类在使用时就可以很轻松的用NSOperationQueue来实现多线程下载多张图片。该类逻辑也很简单,加入到NSOperationQueue以后,执行start方法时就会通过一个可用的NSURLSession对象来创建一个NSURLSessionDataTask的下载任务,并设置回调,在回调方法中接收数据并进行一系列通知和触发回调块。

源码很多地方都用到了SDWebImage自己的编解码技术,所以又去了解了一下相关知识。在展示一张图片的时候常使用imageNamed:这样的类方法去获取并展示这张图片,但是图片是以二进制的格式保存在磁盘或内存中的,如果要展示一张图片需要根据图片的不同格式去解码为正确的位图交由系统控件来展示,而解码的操作默认是放在主线程执行。凡是放在主线程执行的任务都务必需要考虑清楚,如果有大量图片要展示,就会在主线程中执行大量的解码任务,势必会阻塞主线程造成卡顿,所以SDWebImage自己实现相关的编解码操作,并在子线程中处理,就不会影响主线程的相关操作。

对于同步代码块有点不解,望理解的读者周知。SDWebImage下载的逻辑也挺简单的,本类SDWebImageDownloaderOperationNSOperation的子类,所以可以使用NSOperationQueue来实现多线程下载。但是每一个Operation类对应一个NSURLSessionTask的下载任务,也就是说,SDWebImageDownloader类在需要下载图片的时候就创建一个Operation, 然后将这个Operation加入到OperationQueue中,就会执行start方法,start方法会创建一个Task来实现下载。所以整个下载任务有两个子线程,一个是Operation执行start方法的线程用来开启Task的下载任务,一个是Task的线程来执行下载任务。OperationTask是一对一的关系,应该不会有竞争条件产生呀?


1、属性与方法声明

a、全局变量
// 进度回调块和下载完成回调块的字符串类型的key
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

// 定义了一个可变字典类型的回调块集合,这个字典key的取值就是上面两个字符串,value就是回调块了
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

b、下载任务协议
开发者可以实现自己的下载操作只需要实现该协议即可
@protocol SDWebImageDownloaderOperation <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

// 初始化函数,根据指定的request、session和下载选项创建一个下载任务
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

// 添加进度和完成后的回调块
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

@end
SDWebImageDownloaderOperation类继承自NSOperation并遵守了SDWebImageDownloaderOperation协议
// 该类继承自NSOperation主要是为了将任务加进并发队列里实现多线程下载多张图片
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperation>

c、公开的属性
// 下载任务的request
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;

// 连接服务端后的收到的响应
@property (strong, nonatomic, readonly, nullable) NSURLResponse *response;

// 执行下载操作的下载任务
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;

// https需要使用的凭证
@property (strong, nonatomic, nullable) NSURLCredential *credential;

// 下载时配置的相关内容
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;

d、公开的方法
// 初始化方法需要下载文件的request、session以及下载相关配置选项
// 真正实现下载操作的是NSURLSessionTask类的子类,这里就可以看出SDWebImage使用NSURLSession实现下载图片的功能
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

// 添加一个进度回调块和下载完成后的回调块
// 返回一个token,用于取消这个下载任务,这个token其实是一个字典
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

// 这个方法不是用来取消下载任务的,而是删除前一个方法添加的进度回调块和下载完成回调块,当所有的回调块都删除后,下载任务也会被取消
// 需要传入上一个方法返回的token
- (BOOL)cancel:(nullable id)token;

e、私有属性
回调块数组,数组内的元素即为前面自定义的字典
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
继承NSOperation需要定义executing和finished属性,并实现getter和setter,手动触发KVO通知
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
图片数据
// 可变NSData数据,存储下载的图片数据
@property (strong, nonatomic, nullable) NSMutableData *imageData;
// 缓存的图片数据
@property (copy, nonatomic, nullable) NSData *cachedData;
// 需要下载的文件的大小
@property (assign, nonatomic) NSUInteger expectedSize;
// 接收到下载的文件的大小
@property (assign, nonatomic) NSUInteger receivedSize;
// 上一进度百分比
@property (assign, nonatomic) double previousProgress;
连接服务端后的收到的响应
@property (strong, nonatomic, nullable, readwrite) NSURLResponse *response;
@property (strong, nonatomic, nullable) NSError *responseError;
// 修改原始URL响应
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
NSURLSession属性
/*
这里是weak修饰的NSURLSession属性
作者解释到unownedSession有可能不可用,因为这个session是外面传进来的,由其他类负责管理这个session,本类不负责管理
这个session有可能会被回收,当不可用时使用下面那个session
*/
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;

/*
 strong修饰的session,当上面weak的session不可用时,需要创建一个session
 这个session需要由本类负责管理,需要在合适的地方调用invalid方法打破引用循环
 */
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
图像解码
// 图像解码的串行操作队列
@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue;
// 解密图像数据
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor;
下载任务
// 具体的下载任务
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;
// iOS上支持在后台下载时需要一个identifier
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;

f、初始化方法
合成存取了executing和finished属性
@synthesize executing = _executing;
@synthesize finished = _finished;
初始化方法
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context
{
    if ((self = [super init]))
    {
        _request = [request copy];
        _options = options;
        _context = [context copy];
        _callbackBlocks = [NSMutableArray new];
        _responseModifier = context[SDWebImageContextDownloadResponseModifier];
        _decryptor = context[SDWebImageContextDownloadDecryptor];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        // 在初始化方法中将传入的session赋给了unownedSession,所以这个session是外部传入的,本类就不需要负责管理它
        // 但是它有可能会被释放,所以当这个session不可用时需要自己创建一个新的session并自行管理
        _unownedSession = session;
        _coderQueue = [NSOperationQueue new];
        _coderQueue.maxConcurrentOperationCount = 1;
        _backgroundTaskId = UIBackgroundTaskInvalid;
    }
    return self;
}

2、回调块

a、添加进度回调块和下载完成回调块

往一个字典类型的数组中添加回调块,这个字典最多只有两个key-value键值对,数组中可以有多个这样的字典,每添加一个进度回调块和下载完成回调块就会把这个字典返回作为token

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
{
    // 创建一个<NSString,id>类型的可变字典,value为回调块
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    // 如果进度回调块存在就加进字典里,key为@"progress"
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    // 如果下载完成回调块存在就加进字典里,key为@"completed"
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    // 阻塞并发队列,串行执行添加进数组的操作
    @synchronized (self)
    {
        [self.callbackBlocks addObject:callbacks];
    }
    // 回的token其实就是这个字典
    return callbacks;
}

b、通过key获取回调块数组中所有对应key的回调块
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key
{
    NSMutableArray<id> *callbacks;
    // 同步方式执行,阻塞当前线程也阻塞队列
    @synchronized (self)
    {
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
    }
    // 如果字典中没有对应key会返回null,所以需要删除为null的元素
    [callbacks removeObjectIdenticalTo:[NSNull null]];
    return [callbacks copy];
}

c、取消方法

在取消任务方法中会从数组中删除掉这个字典,但是只有当数组中的回调块字典全部被删除完了才会真正取消任务。

- (BOOL)cancel:(nullable id)token
{
    if (!token) return NO;
    
    BOOL shouldCancel = NO;
    // 同步方式执行,阻塞当前线程也阻塞队列
    @synchronized (self)
    {
        // 根据token删除数组中的数据,token就是key为string,value为block的字典
        NSMutableArray *tempCallbackBlocks = [self.callbackBlocks mutableCopy];
        // 删除的就是数组中的字典元素
        [tempCallbackBlocks removeObjectIdenticalTo:token];
        // 如果回调块数组长度为0就真的要取消下载任务了,因为已经没有人来接收下载完成和下载进度的信息,下载完成也没有任何意义
        if (tempCallbackBlocks.count == 0)
        {
            shouldCancel = YES;
        }
    }
    
    // 如果要真的要取消任务就调用cancel方法
    if (shouldCancel)
    {
        [self cancel];
    }
    
    return shouldCancel;
}

3、启动下载任务

重写NSOperation类的start方法,任务添加到NSOperationQueue后会执行该方法,启动下载任务。判断session是否可用然后决定是否要自行管理一个NSURLSession对象,接下来就使用这个session创建一个NSURLSessionDataTask对象,这个对象是真正执行下载和服务端交互的对象,接下来就开启这个下载任务然后进行通知和回调块的触发工作。

- (void)start
{
    ...
}
a、同步代码块,防止产生竞争条件

NSOperation子类加进NSOperationQueue后会自行调用start方法,并且只会执行一次,不太理解为什么需要加这个。

@synchronized (self)
{
    ...
}
❶ 判断是否取消了下载任务
if (self.isCancelled)
{
    // 如果取消了就设置finished为YES,
    self.finished = YES;
    // 用户取消错误
    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]];
    // 调用reset方法
    [self reset];
    return;
}
❷ iOS支持可以在app进入后台后继续下载
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground])
{
    __weak typeof(self) wself = self;
    UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
    self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
        [wself cancel];
    }];
}
❸ 判断unownedSession是否为nil,为空则自行创建一个NSURLSession对象
NSURLSession *session = self.unownedSession;
if (!session)
{
    // 为空则自行创建一个NSURLSession对象
    // session运行在默认模式下
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    // 超时时间15s
    sessionConfig.timeoutIntervalForRequest = 15;
    
    // delegateQueue为nil,所以回调方法默认在一个子线程的串行队列中执行
    session = [NSURLSession sessionWithConfiguration:sessionConfig
                                            delegate:self
                                       delegateQueue:nil];
    // 局部变量赋值
    self.ownedSession = session;
}
❹ 根据配置的下载选项获取网络请求的缓存数据
if (self.options & SDWebImageDownloaderIgnoreCachedResponse)
{
    NSURLCache *URLCache = session.configuration.URLCache;
    if (!URLCache)
    {
        URLCache = [NSURLCache sharedURLCache];
    }
    NSCachedURLResponse *cachedResponse;
    @synchronized (URLCache)
    {
        cachedResponse = [URLCache cachedResponseForRequest:self.request];
    }
    if (cachedResponse)
    {
        self.cachedData = cachedResponse.data;
    }
}
❺ 使用可用的session来创建一个NSURLSessionDataTask类型的下载任务
self.dataTask = [session dataTaskWithRequest:self.request];
❻ 设置NSOperation子类的executing属性,标识开始下载任务
self.executing = YES;

b、开始执行任务
❶ 如果这个NSURLSessionDataTask不为空即开启成功
if (self.dataTask)
{
    ...
}
❷ 设置任务执行优先级
if (self.options & SDWebImageDownloaderHighPriority)
{
    // 设置任务优先级为高优先级
    self.dataTask.priority = NSURLSessionTaskPriorityHigh;
    // 图像解码的串行操作队列的服务质量为用户交互
    self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
}
else if (self.options & SDWebImageDownloaderLowPriority)
{
    self.dataTask.priority = NSURLSessionTaskPriorityLow;
    self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
}
else
{
    self.dataTask.priority = NSURLSessionTaskPriorityDefault;
    self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
❸ NSURLSessionDataTask任务开始执行
[self.dataTask resume];
❹ 遍历所有的进度回调块并执行
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
{
    progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
❺ 在主线程中发送通知,并将self传出去
__block typeof(self) strongSelf = self;
// 在什么线程发送通知,就会在什么线程接收通知
// 为了防止其他监听通知的对象在回调方法中修改UI,这里就需要在主线程中发送通知
dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});

4、设置取消与完成下载任务

a、取消下载任务
❶ SDWebImageOperation协议的cancel方法,取消任务,调用cancelInternal方法
- (void)cancel
{
    @synchronized (self)
    {
        // 真正取消下载任务的方法
        [self cancelInternal];
    }
}
❷ 真正取消下载任务的方法
- (void)cancelInternal
{
    // 如果下载任务已经结束了直接返回
    if (self.isFinished) return;
    
    // 调用NSOperation类的cancel方法,即将isCancelled属性置为YES
    [super cancel];

    // 如果NSURLSessionDataTask下载图片的任务存在
    if (self.dataTask)
    {
        // 调用其cancel方法取消下载任务
        [self.dataTask cancel];
        
        // 在主线程中发出下载停止的通知
        __block typeof(self) strongSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
        });
        
        // 设置两个属性的值
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    // 调用reset方法
    [self reset];
}

b、完成下载任务
❶ 下载完成后调用的方法
- (void)done
{
    // 设置finished为YES executing为NO
    self.finished = YES;
    self.executing = NO;
    
    // 调用reset方法
    [self reset];
}
❷ NSOperation子类finished属性的setter:手动触发KVO通知
- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}
❸ NSOperation子类isExecuting属性的setter:手动触发KVO通知
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
❹ 重写NSOperation方法,标识这是一个并发任务
- (BOOL)isConcurrent
{
    return YES;
}

c、大侠请重新来过
- (void)reset
{
    @synchronized (self)
    {
        // 删除回调块字典数组的所有元素
        [self.callbackBlocks removeAllObjects];
        // NSURLSessionDataTask对象置为nil
        self.dataTask = nil;
        
        // 如果ownedSession存在,就需要我们手动调用invalidateAndCancel方法打破引用循环
        if (self.ownedSession)
        {
            [self.ownedSession invalidateAndCancel];
            self.ownedSession = nil;
        }
        
        // 停止后台下载
        if (self.backgroundTaskId != UIBackgroundTaskInvalid)
        {
            UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
            [app endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    }
}

5、NSURLSessionDataDelegate 代理方法

下面几个方法就是在接收到服务端响应后进行一个处理,判断是否是正常响应,如果是正常响应就进行各种赋值和初始化操作,并触发回调块,进行通知等操作,如果不是正常响应就结束下载任务。接下来的一个比较重要的方法就是接收到图片数据的处理,接收到数据后就追加到可变数据中,如果需要在图片没有下载完成时就展示部分图片,需要进行一个解码的操作然后调用回调块将图片数据回传,接着就会调用存储的进度回调块来通知现在的下载进度,回传图片的总长度和已经下载长度的信息。

a、收到服务端响应,在一次请求中只会执行一次
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    ...
}
❶ 修改原始URL响应
if (self.responseModifier && response)
{
    response = [self.responseModifier modifiedResponseWithResponse:response];
    if (!response)
    {
        valid = NO;
        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}];
    }
}
// 将连接服务端后的收到的响应赋值到成员变量
self.response = response;
❷ 根据http状态码判断是否成功响应,如果响应不正常触发异常回调块。需要注意的是304被认为是异常响应
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
BOOL statusCodeValid = statusCode >= 200 && statusCode < 400;
if (!statusCodeValid)
{
    valid = NO;
    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}];
}

if (statusCode == 304 && !self.cachedData)
{
    valid = NO;
    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Download response status code is 304 not modified and ignored"}];
}
❸ 如果响应正常遍历进度回调块并触发进度回调块
// 获取要下载图片的长度
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
// 设置长度
self.expectedSize = expected;

// 如果响应正常
if (valid)
{
    // 遍历进度回调块并触发进度回调块
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
    {
        progressBlock(0, expected, self.request.URL);
    }
}
❹ 如果响应不正常则直接取消下载任务
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;

if (valid)
{
    ...
}
else
{
    disposition = NSURLSessionResponseCancel;
}
❺ 如果有回调块就执行
if (completionHandler)
{
    completionHandler(disposition);
}

b、收到数据的回调方法,可能执行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    ...
}
❶ 向可变数据中添加接收到的数据
if (!self.imageData)
{
    self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];
❷ 计算下载进度
// 获取已经下载了多大的数据
self.receivedSize = self.imageData.length;
// 判断是否已经下载完成
BOOL finished = (self.receivedSize >= self.expectedSize);
// 计算下载进度
double currentProgress = (double)self.receivedSize / (double)self.expectedSize;
double previousProgress = self.previousProgress;
double progressInterval = currentProgress - previousProgress;
// 龟速下载直接返回
if (!finished && (progressInterval < self.minimumProgressInterval))
{
    return;
}
self.previousProgress = currentProgress;
❸ 渐进式解码
// 使用数据解密将禁用渐进式解码
BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
// 支持渐进式解码
if (supportProgressive)
{
    // 获取图像数据
    NSData *imageData = [self.imageData copy];
    
    // 下载期间最多保留一个按照下载进度进行解码的操作
    // coderQueue是图像解码的串行操作队列
    if (self.coderQueue.operationCount == 0)
    {
        // NSOperation有自动释放池,不需要额外创建一个
        [self.coderQueue addOperationWithBlock:^{
            // 将数据交给解码器返回一个图片
            UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
            
            if (image)
            {
                // 触发回调块回传这个图片
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }];
    }
}
❹ 调用进度回调块并触发进度回调块
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
{
    progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}

c、如果要缓存响应时回调该方法
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    NSCachedURLResponse *cachedResponse = proposedResponse;

    // 如果request的缓存策略是不缓存本地数据就设置为nil
    if (!(self.options & SDWebImageDownloaderUseNSURLCache))
    {
        // 防止缓存响应,避免进行本地缓存
        cachedResponse = nil;
    }
    
    // 调用回调块
    if (completionHandler)
    {
        completionHandler(cachedResponse);
    }
}

6、NSURLSessionTaskDelegate 代理方法

a、下载完成或下载失败时的回调方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    ...
}
❶ 主线程根据error是否为空发送对应通知
@synchronized(self)
{
    // 置空
    self.dataTask = nil;
    
    // 主线程根据error是否为空发送对应通知
    __block typeof(self) strongSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
        if (!error)
        {
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf];
        }
    });
}
❷ 如果error存在,即下载过程中出错
// 自定义错误而不是URLSession错误
if (self.responseError)
{
    error = self.responseError;
}
// 触发对应回调块
[self callCompletionBlocksWithError:error];
// 下载完成后调用的方法
[self done];
❸ 下载成功则对图片进行解码
// 判断下载完成回调块个数是否大于0
if ([self callbacksForKey:kCompletedCallbackKey].count > 0)
{
    // 获取不可变data图片数据
    NSData *imageData = [self.imageData copy];
    self.imageData = nil;
    // 如果下载的图片和解密图像数据的解码器存在
    if (imageData && self.decryptor)
    {
        // 解码图片,返回data
        imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
    }
    ...
}
❹ 如果下载设置为只使用缓存数据就会判断缓存数据与当前获取的数据是否一致,一致就触发完成回调块
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData])
{
    // 错误:下载的图像不会被修改和忽略
    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}];
    // 调用带有未修改错误的回调完成块
    [self callCompletionBlocksWithError:self.responseError];
    [self done];
}
❺ 解码图片,返回图片
// 取消之前的所有解码过程
[self.coderQueue cancelAllOperations];

// 图像解码的串行操作队列
[self.coderQueue addOperationWithBlock:^{
    // 解码图片,返回图片
    UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);

    CGSize imageSize = image.size;
    // 下载的图像有0个像素
    if (imageSize.width == 0 || imageSize.height == 0)
    {
        // 调用带有图像大小为0错误的回调完成块
        NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
    }
    else
    {
        // 触发成功完成回调块
        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
    }
    // 下载完成后调用的方法
    [self done];
}];

b、如果是https访问就需要设置SSL证书相关
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    
    ...
    
    if (completionHandler)
    {
        completionHandler(disposition, credential);
    }
}

c、遍历所有的完成回调块,在主线程中触发
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished
{
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

续文见下篇:IOS源码解析:SDWeblmage(下)


Demo

Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容