IOS源碼解析:SDWeblmage (上)

原創(chuàng):知識(shí)點(diǎn)總結(jié)性文章
創(chuàng)作不易,請(qǐng)珍惜,之后會(huì)持續(xù)更新,不斷完善
個(gè)人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長(zhǎng)歷程,希望能與大家一起進(jìn)步
溫馨提示:由于簡(jiǎn)書不支持目錄跳轉(zhuǎn),大家可通過(guò)command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容

目錄

  • 一、系統(tǒng)提供的NSURLSession的使用
    • 1、簡(jiǎn)介
    • 2、Get請(qǐng)求
    • 3、發(fā)送POST請(qǐng)求
    • 4、下載文件
    • 5、監(jiān)聽具體請(qǐng)求狀態(tài)的代理
  • 二、認(rèn)識(shí)SDWeblmage框架
    • 1、簡(jiǎn)介
    • 2、使用方法
    • 3、目錄結(jié)構(gòu)
    • 4、sd_setImageWithURL:核心方法
    • 5、拓展
  • 四、SDWebImageDownloader 管理所有的下載任務(wù)
    • 1、屬性與枚舉
    • 2、Lifecycle
    • 3、Setter和Getter
    • 4、核心方法:下載圖片
  • 五、SDImageDownloaderOperation 具體的下載任務(wù)
    • 1、屬性與方法
    • 2、回調(diào)塊
    • 3、啟動(dòng)下載任務(wù)
    • 4、設(shè)置取消與完成下載任務(wù)
    • 5、NSURLSessionDataDelegate 代理方法
    • 6、NSURLSessionTaskDelegate 代理方法
  • Demo
  • 參考文獻(xiàn)

一、系統(tǒng)提供的NSURLSession的使用

1、簡(jiǎn)介

NSURLSession工作在OSI 七層模型的會(huì)話層。會(huì)話層之下的所有工作,系統(tǒng)都已經(jīng)幫我們做好了,所以這里的Session也可以理解為會(huì)話。NSURLSession提供了豐富的類來(lái)支持GET/POST請(qǐng)求、支持后臺(tái)下載和上傳,可將文件直接下載到磁盤的沙盒中。

為了方便使用,NSURLSession提供了一個(gè)單例的方法來(lái)獲取一個(gè)全局共享的session對(duì)象,接下來(lái)通過(guò)這個(gè)session對(duì)象構(gòu)造了一個(gè)請(qǐng)求任務(wù),即NSURLSessionDataTask類的對(duì)象。這個(gè)類是NSURLSessionTask的子類,主要用于進(jìn)行一些比較簡(jiǎn)短數(shù)據(jù)的獲取,通常用于發(fā)送GET/POST請(qǐng)求。默認(rèn)發(fā)起GET請(qǐng)求,如果需要發(fā)起POST請(qǐng)求需要額外的操作。創(chuàng)建的任務(wù)默認(rèn)是掛起狀態(tài)的,所以為了啟動(dòng)網(wǎng)絡(luò)請(qǐng)求,調(diào)用其resume方法即可開始執(zhí)行請(qǐng)求。當(dāng)任務(wù)完成時(shí)就會(huì)執(zhí)行上述回調(diào)塊,當(dāng)然也可以使用代理的方式監(jiān)聽網(wǎng)絡(luò)請(qǐng)求。這樣看來(lái)它的使用真的很方便,并且默認(rèn)會(huì)自動(dòng)開啟多線程異步執(zhí)行。下面栗子的回調(diào)塊中輸出了當(dāng)前線程可以看出并不是主線程,所以在回調(diào)中如果要進(jìn)行UI的更新操作需要放到主線程中執(zhí)行。

NSURLSession也提供了豐富的代理來(lái)監(jiān)聽具體請(qǐng)求的狀態(tài)。我們無(wú)法為全局共享的NSURLSession對(duì)象設(shè)置代理,也就不能監(jiān)聽其網(wǎng)絡(luò)請(qǐng)求。原因很簡(jiǎn)單,委托對(duì)象只有一個(gè),而全局共享的單例對(duì)象可能有很多類都在使用。所以只能自己創(chuàng)建一個(gè)NSURLSession對(duì)象并在初始化方法中指定其委托對(duì)象。

NSURLSessionTask類似抽象類不提供網(wǎng)絡(luò)請(qǐng)求的功能,具體實(shí)現(xiàn)由其子類實(shí)現(xiàn)。上面的栗子使用的就是NSURLSessionDataTask主要用來(lái)獲取一些簡(jiǎn)短的數(shù)據(jù),如發(fā)起GET/POST請(qǐng)求。NSURLSessionDownloadTask用于下載文件,它提供了很多功能,默認(rèn)支持將文件直接下載至磁盤沙盒中,這樣可以避免占用過(guò)多內(nèi)存的問(wèn)題。NSURLSessionUploadTask用于上傳文件。NSURLSessionStreamTask提供了以流的形式讀寫TCP/IP流的功能,可以實(shí)現(xiàn)異步讀寫的功能。前面三個(gè)類使用的比較頻繁,在SDWebImage中用于下載圖片的具體任務(wù)是交由NSURLSessionDataTask完成。由于緩存策略的問(wèn)題,圖片一般都較小,可能不需要將圖片保存至磁盤,所以也就不需要使用NSURLSessionDownloadTask


2、Get請(qǐng)求

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(@"數(shù)據(jù):%@,錯(cuò)誤:%@,線程:%@", data, error, [NSThread currentThread]);
    NSLog(@"回應(yīng):%@", response);
}];
[task resume];

輸出結(jié)果為:

2021-02-02 16:35:05.560444+0800 SDWebImageSourceCodeAnalysis[77552:10355246] 數(shù)據(jù):{length = 3603, bytes = 0x7b226368 616e6e65 6c73223a 5b7b226e ... 6e223a22 227d5d7d },錯(cuò)誤:(null),線程:<NSThread: 0x60000300d800>{number = 6, name = (null)}
2021-02-02 16:35:05.560672+0800 SDWebImageSourceCodeAnalysis[77552:10355246] 回應(yīng):<NSHTTPURLResponse: 0x60000250e1c0> { URL: https://www.douban.com/j/app/radio/channels } { Status Code: 200, Headers {
    "Cache-Control" =     (
        "must-revalidate, no-cache, private"
    );
...

3、發(fā)送POST請(qǐng)求

// 創(chuàng)建NSURL的請(qǐng)求路徑URL
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8080/login"];

// 創(chuàng)建一個(gè)可變的request對(duì)象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

// 修改請(qǐng)求方式為POST方法,默認(rèn)是GET方法
[request setHTTPMethod:@"POST"];

// 設(shè)置請(qǐng)求體,即添加post請(qǐng)求數(shù)據(jù)
[request setHTTPBody:[@"username=xiejiapei&password=Sgahd" dataUsingEncoding:NSUTF8StringEncoding]];

// 使用單例的全局共享的session對(duì)象
NSURLSession *session = [NSURLSession sharedSession];

// 使用上述request構(gòu)造一個(gè)任務(wù)對(duì)象
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    // 請(qǐng)求完成后會(huì)執(zhí)行回調(diào)塊,可以根據(jù)服務(wù)端返回的數(shù)據(jù)轉(zhuǎn)換為JSON數(shù)據(jù)或者HTML等格式
    NSLog(@"數(shù)據(jù):%@,錯(cuò)誤:%@,線程:%@", data, error, [NSThread currentThread]);
    NSLog(@"回應(yīng):%@", response);
}];

// 啟動(dòng)任務(wù)
[task resume];

4、下載文件

// 創(chuàng)建文件地址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對(duì)象
NSURLSession *session = [NSURLSession sharedSession];

// 創(chuàng)建一個(gè)下載任務(wù)
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    
   // 這個(gè)location就是下載到磁盤的位置,默認(rèn)是在沙盒tmp文件夾中
    NSLog(@"下載到磁盤的位置:%@", location);
    
    // tmp文件夾在關(guān)閉app后會(huì)自動(dòng)刪除,有需要可以使用NSFileManager將該文件轉(zhuǎn)移到沙盒其他目錄下
    NSFileManager *fileManager = [NSFileManager defaultManager];
    [fileManager copyItemAtPath:location.path toPath:@"..." error:nil];
}];
// 啟動(dòng)任務(wù)
[downloadTask resume];

輸出結(jié)果為:

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、監(jiān)聽具體請(qǐng)求狀態(tài)的代理

a、設(shè)置代理和代理方法執(zhí)行隊(duì)列

Foundation框架提供了三種NSURLSession的運(yùn)行模式,即三種NSURLSessionConfiguration會(huì)話配置。defaultSessionConfiguration默認(rèn)Session運(yùn)行模式,使用該配置默認(rèn)使用磁盤緩存網(wǎng)絡(luò)請(qǐng)求相關(guān)數(shù)據(jù)如cookie等信息。ephemeralSessionConfiguration臨時(shí)Session運(yùn)行模式,不緩存網(wǎng)絡(luò)請(qǐng)求的相關(guān)數(shù)據(jù)到磁盤,只會(huì)放到內(nèi)存中使用。backgroundSessionConfiguration后臺(tái)Session運(yùn)行模式,如果需要實(shí)現(xiàn)在后臺(tái)繼續(xù)下載或上傳文件時(shí)需要使用該會(huì)話配置,需要配置一個(gè)唯一的字符串作為區(qū)分。同時(shí),NSURLSessionConfiguration還可以配置一些其他信息,如緩存策略、超時(shí)時(shí)間、是否允許蜂窩網(wǎng)絡(luò)訪問(wèn)等信息。

@interface ViewController ()<NSURLSessionDelegate>

// 創(chuàng)建一個(gè)代理方法執(zhí)行的隊(duì)列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 設(shè)置隊(duì)列的名稱
queue.name = @"MyDelegateQueue";

// 創(chuàng)建一個(gè)session,運(yùn)行在默認(rèn)模式下
// 設(shè)置代理和代理方法執(zhí)行隊(duì)列
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:queue];

// 創(chuàng)建一個(gè)任務(wù)
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
// 啟動(dòng)任務(wù)
[task resume];

b、收到服務(wù)端響應(yīng)時(shí)執(zhí)行,一次請(qǐng)求只會(huì)執(zhí)行一次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    NSLog(@"Receive Response %@ %@ %@", response, [NSThread currentThread], [NSOperationQueue currentQueue]);
    
    // 如果要實(shí)現(xiàn)這個(gè)代理方法一定要執(zhí)行這個(gè)回調(diào)塊,如果不執(zhí)行這個(gè)回調(diào)塊默認(rèn)就會(huì)取消任務(wù),后面就不會(huì)從服務(wù)器獲取數(shù)據(jù)了,后面的回調(diào)方法都不會(huì)再執(zhí)行
    if (completionHandler)
    {
        completionHandler(NSURLSessionResponseAllow);
    }
}

c、從服務(wù)端收到數(shù)據(jù),一次請(qǐng)求中可能執(zhí)行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    NSLog(@"Receive Data %@",  [NSOperationQueue currentQueue]);
}

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

e、輸出結(jié)果

從輸出結(jié)果看代理方法都是在子線程中執(zhí)行,執(zhí)行的隊(duì)列也是我們創(chuàng)建的隊(duì)列,如果需要在主線程中執(zhí)行代理就將代理隊(duì)列設(shè)置為主隊(duì)列即可。

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'}

二、認(rèn)識(shí)SDWeblmage框架

1、簡(jiǎn)介

a、設(shè)計(jì)目的

SDWebImage提供了 UIImageViewUIButton、MKAnnotationView的圖片下載分類,只要一行代碼就可以實(shí)現(xiàn)圖片異步下載和緩存功能。這樣開發(fā)者就無(wú)須花太多精力在圖片下載細(xì)節(jié)上,專心處理業(yè)務(wù)邏輯。


b、特性
  • 異步下載圖片,不阻塞主線程
  • 異步緩存(內(nèi)存+磁盤),自動(dòng)管理緩存有效性
  • 在后臺(tái)進(jìn)行圖片解壓縮
  • 同一個(gè) URL 不會(huì)重復(fù)下載,并且自動(dòng)識(shí)別無(wú)效 URL,不會(huì)反復(fù)重試
  • 支持多種圖片格式,并支持動(dòng)圖(GIF

2、使用方法

a、sd_setImageWithURL

block中得到圖片下載進(jìn)度和圖片加載完成(下載完成或者讀取緩存)的回調(diào),如果你在圖片加載完成前取消了請(qǐng)求操作,就不會(huì)收到成功或失敗的回調(diào)。

[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:支持內(nèi)存緩存和磁盤緩存
? 添加緩存的方法
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
? 默認(rèn)情況下,圖片數(shù)據(jù)會(huì)同時(shí)緩存到內(nèi)存和磁盤中,如果你想只要內(nèi)存緩存的話,可以使用下面的方法
[[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

有時(shí)候,一張圖片的 URL 中的一部分可能是動(dòng)態(tài)變化的(比如獲取權(quán)限上的限制),所以我們只需要把 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、目錄結(jié)構(gòu)

Downloader:下載
  • SDWebImageDownloader:專門用來(lái)下載圖片的,跟緩存沒有關(guān)系
  • SDWebImageDownloaderOperation:繼承于 NSOperation,用來(lái)處理下載任務(wù)的
Cache:緩存
  • SDImageCache:用來(lái)處理內(nèi)存緩存和磁盤緩存(可選)的,其中磁盤緩存是異步進(jìn)行的,因此不會(huì)阻塞主線程
Utils:工具類
  • SDWebImageManager:作為 UIImageView+WebCache 背后的默默付出者,主要功能是將圖片下載(SDWebImageDownloader)和圖片緩存(SDImageCache)兩個(gè)獨(dú)立的功能組合起來(lái)
  • SDWebImageDecoder:圖片解碼器,用于圖片下載完成后進(jìn)行解碼
  • SDWebImagePrefetcher:預(yù)下載圖片,方便后續(xù)使用
Categories:分類
  • UIView+WebCacheOperation:用來(lái)記錄圖片加載的 operation,方便需要時(shí)取消和移除圖片加載的 operation
  • UIImageView+WebCache:集成 SDWebImageManager 的圖片下載和緩存功能到 UIImageView 的方法中,方便調(diào)用
  • UIImageView+HighlightedWebCache:也是包裝了 SDWebImageManager,只不過(guò)是用于加載 highlighted 狀態(tài)的圖片
  • UIButton+WebCache:集成 SDWebImageManager 的圖片下載和緩存功能到 UIButton 的方法中
  • MKAnnotationView+WebCache:集成 SDWebImageManager 的圖片下載和緩存功能到 MKAnnotationView 的方法中
  • NSData+ImageContentType:用于獲取圖片數(shù)據(jù)的格式(JPEGPNG等)
  • UIImage+GIF:用于加載 GIF 動(dòng)圖
  • UIImage+MultiFormat:將不同格式的二進(jìn)制數(shù)據(jù)轉(zhuǎn)成 UIImage 對(duì)象
  • UIImage+WebP:用于解碼并加載 WebP 圖片
Other:其他
  • SDWebImageOperation(協(xié)議)
  • SDWebImageCompat(宏定義、常量、通用函數(shù))

4、sd_setImageWithURL:核心方法

a、外界調(diào)用
[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、取消當(dāng)前正在進(jìn)行的加載任務(wù)
[self sd_cancelCurrentImageLoad];
c、通過(guò)關(guān)聯(lián)對(duì)象將 url 作為成員變量存起來(lái)
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
d、設(shè)置占位圖
if (!(options & SDWebImageDelayPlaceholder))
{
    dispatch_main_async_safe(^{
        self.image = placeholder;
    });
}
e、URL 為空時(shí),直接回調(diào) completedBlock,返回錯(cuò)誤信息
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 不為空
? 調(diào)用 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)
{
    ...
}
? 如果不需要自動(dòng)設(shè)置 image,直接 return
dispatch_main_sync_safe(^{
    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
    {
        completedBlock(image, error, cacheType, url);
        return;
    }
    ...
});
? 圖片下載成功,設(shè)置 image
wself.image = image;
[wself setNeedsLayout];
? 圖片下載失敗,設(shè)置 placeholder
if ((options & SDWebImageDelayPlaceholder))
{
    wself.image = placeholder;
    [wself setNeedsLayout];
}
? 回調(diào) completedBlock
if (completedBlock && finished)
{
    completedBlock(image, error, cacheType, url);
}
g、借助 UIView+WebCacheOperation 將獲得的 operation 保存到成員變量中去
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

5、拓展

后臺(tái)下載

使用-[UIApplication beginBackgroundTaskWithExpirationHandler:]方法使 app 退到后臺(tái)時(shí)還能繼續(xù)執(zhí)行任務(wù),不再執(zhí)行后臺(tái)任務(wù)時(shí),需要調(diào)用 -[UIApplication endBackgroundTask:] 方法標(biāo)記后臺(tái)任務(wù)結(jié)束。

文件的緩存有效期及最大緩存空間大小

默認(rèn)有效期:

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

默認(rèn)最大緩存空間:

maxCacheSize = <#unlimited#>
MKAnnotationView

MKAnnotationView 是屬于 MapKit 框架的一個(gè)類,繼承自UIView,是用來(lái)展示地圖上的annotation 信息的,它有一個(gè)用來(lái)設(shè)置圖片的屬性 image。

假如自己來(lái)實(shí)現(xiàn)一個(gè)圖片下載工具,該怎么寫?

圖片讀寫:以圖片URL的單向Hash值作為Key
淘汰策略:以隊(duì)列先進(jìn)先出的方式淘汰,LRU算法(如30分鐘之內(nèi)是否使用過(guò))
磁盤設(shè)計(jì):存儲(chǔ)方式、大小限制(如100MB )、淘汰策略(如某圖片存儲(chǔ)時(shí)間距今已超過(guò)7天)
網(wǎng)絡(luò)設(shè)計(jì):圖片請(qǐng)求最大并發(fā)量、請(qǐng)求超時(shí)策略、請(qǐng)求優(yōu)先級(jí)
圖片解碼:對(duì)于不同格式的圖片,解碼采用什么方式來(lái)做? 在哪個(gè)階段做圖片解碼處理?(磁盤讀取后網(wǎng)絡(luò)請(qǐng)求返回后)


四、SDWebImageDownloader 管理所有的下載任務(wù)

  • 如何實(shí)現(xiàn)異步下載,也就是多張圖片同時(shí)下載?
  • 如何處理同一張圖片(同一個(gè) URL)多次下載的情況?

1、屬性與方法

a、輔助變量
聲明通知的全局變量名
NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
下載選項(xiàng)
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0, //低優(yōu)先級(jí)
    SDWebImageDownloaderProgressiveDownload = 1 << 1, //帶有下載進(jìn)度
    SDWebImageDownloaderUseNSURLCache = 1 << 2, //使用緩存
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, //忽略緩存響應(yīng)
    SDWebImageDownloaderContinueInBackground = 1 << 4, //支持后臺(tái)下載
    SDWebImageDownloaderHandleCookies = 1 << 5, //使用Cookies
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, //允許驗(yàn)證SSL
   SDWebImageDownloaderHighPriority = 1 << 7, //高優(yōu)先級(jí)
};

下載選項(xiàng)枚舉使用了位運(yùn)算。通過(guò)“與”運(yùn)算符,可以判斷是否設(shè)置了某個(gè)枚舉選項(xiàng),因?yàn)槊總€(gè)枚舉選擇項(xiàng)中只有一位是1,其余位都是 0,所以只有參與運(yùn)算的另一個(gè)二進(jìn)制值在同樣的位置上也為 1,與運(yùn)算的結(jié)果才不會(huì)為 0。

0101 (相當(dāng)于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
& 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache)
= 0100 (> 0,也就意味著 option 參數(shù)中設(shè)置了 SDWebImageDownloaderUseNSURLCache)
下載任務(wù)執(zhí)行順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) 
{
    SDWebImageDownloaderFIFOExecutionOrder, //執(zhí)行順序?yàn)橄冗M(jìn)先出
    SDWebImageDownloaderLIFOExecutionOrder //執(zhí)行順序?yàn)楹筮M(jìn)先出
};
回調(diào)塊
// 進(jìn)度回調(diào)塊
typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock;
// 下載完成的回調(diào)塊
typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock;

b、屬性
公開屬性
@property (assign, nonatomic) BOOL shouldDecompressImages; //下載完成后是否需要解壓縮圖片,默認(rèn)為 YES
@property (assign, nonatomic) NSInteger maxConcurrentDownloads; //支持的最大同時(shí)下載圖片的數(shù)量,其實(shí)就是NSOperationQueue支持的最大并發(fā)數(shù)
@property (readonly, nonatomic) NSUInteger currentDownloadCount; //當(dāng)前正在下載圖片的數(shù)量,其實(shí)就是NSOperationQueue的operationCount,即正在執(zhí)行下載任務(wù)的operation的數(shù)量
@property (assign, nonatomic) NSTimeInterval downloadTimeout; //下載時(shí)連接服務(wù)器的超時(shí)時(shí)間,默認(rèn)15s
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //執(zhí)行下載任務(wù)的順序,F(xiàn)IFO或LIFO

@property (strong, nonatomic) NSURLCredential //默認(rèn)的URL credential*urlCredential;
@property (strong, nonatomic) NSString *username; //用戶名,有些圖片下載的時(shí)候需要做用戶認(rèn)證
@property (strong, nonatomic) NSString *password; //密碼
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter; //過(guò)濾http首部的回調(diào)塊
私有屬性
@property (strong, nonatomic) NSOperationQueue *downloadQueue; //圖片下載任務(wù)是放在這個(gè) NSOperationQueue 任務(wù)隊(duì)列中來(lái)管理的
@property (weak, nonatomic) NSOperation *lastAddedOperation; //最近一次添加進(jìn)隊(duì)列的operation,主要用于LIFO時(shí)設(shè)置依賴
@property (assign, nonatomic) Class operationClass; //默認(rèn)是SDWebImageDownloaderOperation
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders; //可變字典,存儲(chǔ)http首部
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue; //一個(gè)GCD的隊(duì)列
@property (strong, nonatomic) NSMutableDictionary *URLCallbacks; //圖片下載的回調(diào) block 都是存儲(chǔ)在這個(gè)屬性中,該屬性是一個(gè)字典,key 是圖片的 URL,value 是一個(gè)數(shù)組,包含每個(gè)圖片的多組回調(diào)信息

c、接口方法
// 類方法,獲取全局共享的單例對(duì)象
+ (SDWebImageDownloader *)sharedDownloader;

// 為http首部設(shè)置值
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;

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

// 設(shè)置下載類的Class,默認(rèn)使用SDWebImageDownloaderOperation,開發(fā)者可以實(shí)現(xiàn)相關(guān)協(xié)議進(jìn)行自定義
- (void)setOperationClass:(Class)operationClass;

// 設(shè)置下載隊(duì)列NSOperationQueue掛起          
- (void)setSuspended:(BOOL)suspended;

/*
下載url對(duì)應(yīng)的圖片
設(shè)置下載配置選項(xiàng)、進(jìn)度回調(diào)塊、下載完成回調(diào)塊
返回一個(gè)token,用于取消對(duì)應(yīng)的下載任務(wù)
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

2、Lifecycle

a、initialize:類方法,類加載的時(shí)候執(zhí)行
+ (void)initialize
{
    ...
}
? 如果導(dǎo)入了SDNetworkActivityIndicator文件,就會(huì)展示一個(gè)小菊花

為了讓 SDNetworkActivityIndicator 文件可以不用導(dǎo)入項(xiàng)目中來(lái)(如果不要的話),這里使用了 runtime 的方式來(lái)實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建類以及調(diào)用方法。

if (NSClassFromString(@"SDNetworkActivityIndicator"))
{
    id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
    ...
}
? 刪除加載通知后重新添加加載通知,防止重復(fù)添加出現(xiàn)異常

這個(gè)方法中主要是通過(guò)注冊(cè)通知讓小菊花監(jiān)聽下載事件來(lái)顯示和隱藏狀態(tài)欄上的網(wǎng)絡(luò)活動(dòng)指示器。

[[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:類方法,返回單例對(duì)象
+ (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;
}
? 默認(rèn)使用SDWebImageDownloaderOperation作為下載任務(wù)Operation
_operationClass = [SDWebImageDownloaderOperation class];
? 設(shè)置需要壓縮下載的圖片
_shouldDecompressImages = YES;
? 設(shè)置下載 operation 的默認(rèn)執(zhí)行順序?yàn)镕IFO(先進(jìn)先出還是先進(jìn)后出)
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
? 初始化 _downloadQueue(下載隊(duì)列)并設(shè)置最大并發(fā)數(shù)為6,即同時(shí)最多可以下載6張圖片
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
? 初始化 _URLCallbacks(下載回調(diào) block 的容器)
_URLCallbacks = [NSMutableDictionary new];
? 設(shè)置下載webp格式圖片的http首部
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
? 初始化 _barrierQueue(創(chuàng)建一個(gè)GCD并發(fā)隊(duì)列)
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
? 設(shè)置默認(rèn)下載超時(shí)時(shí)長(zhǎng) 15s
_downloadTimeout = 15.0;

d、dealloc:析構(gòu)函數(shù)
- (void)dealloc
{
    // NSOperationQueue取消所有的下載操作
    [self.downloadQueue cancelAllOperations];
    // 釋放GCD隊(duì)列
    SDDispatchQueueRelease(_barrierQueue);
}

3、Setter和Getter

a、http首部
獲取http首部的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field
{
    return self.HTTPHeaders[field];
}
為http首部設(shè)置鍵值對(duì)
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field
{
    if (value)
    {
        self.HTTPHeaders[field] = value;
    }
    else
    {
        [self.HTTPHeaders removeObjectForKey:field];
    }
}

b、下載圖片的數(shù)量
設(shè)置最大同時(shí)下載圖片的數(shù)量,即NSOperationQueue最大并發(fā)數(shù)
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads
{
    _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
獲取最大同時(shí)下載圖片的數(shù)量
- (NSInteger)maxConcurrentDownloads
{
    return _downloadQueue.maxConcurrentOperationCount;
}
當(dāng)前正在下載圖片數(shù)量,即NSOperationQueue中正在執(zhí)行的operation數(shù)量
- (NSUInteger)currentDownloadCount
{
    return _downloadQueue.operationCount;
}
設(shè)置operation的Class類對(duì)象
- (void)setOperationClass:(Class)operationClass
{
    _operationClass = operationClass ?: [SDWebImageDownloaderOperation class];
}

c、設(shè)置是否掛起下載隊(duì)列
- (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中為了防止引用循環(huán)和空指針,先weak后strong
    __weak __typeof(self)wself = self;
    ...
    // 返回 createCallback 中創(chuàng)建的 operation(SDWebImageDownloaderOperation)
    return operation;
}
a、直接調(diào)用另一個(gè)方法:addProgressCallback
// 把入?yún)?url、progressBlock 和 completedBlock 傳進(jìn)該方法創(chuàng)建一個(gè)SDWebImageDownloaderOperation類的對(duì)象,并在第一次下載該 URL 時(shí)回調(diào) createCallback。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ...
}];
? 設(shè)置超時(shí)時(shí)間
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0)
{
    timeoutInterval = 15.0;
}
? 創(chuàng)建一個(gè)可變的request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
? 設(shè)置cookie的處理策略
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
? 過(guò)濾http首部
if (wself.headersFilter)
{
    request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else
{
    request.allHTTPHeaderFields = wself.HTTPHeaders;
}
? 傳入網(wǎng)絡(luò)請(qǐng)求和下載選項(xiàng)配置創(chuàng)建DownloaderOperation類的對(duì)象
operation = [[wself.operationClass alloc] initWithRequest:request options:options
? 設(shè)置下載完成后是否需要解壓縮
operation.shouldDecompressImages = wself.shouldDecompressImages;
? 如果設(shè)置了 username 和 password,就給 operation 的下載請(qǐng)求設(shè)置一個(gè) NSURLCredential 認(rèn)證憑證
if (wself.username && wself.password)
{
    operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
? 設(shè)置 operation 的隊(duì)列優(yōu)先級(jí)
if (options & SDWebImageDownloaderHighPriority)
{
    operation.queuePriority = NSOperationQueuePriorityHigh;
}
else if (options & SDWebImageDownloaderLowPriority)
{
    operation.queuePriority = NSOperationQueuePriorityLow;
}
? 將 operation 加入到隊(duì)列 downloadQueue 中,隊(duì)列(NSOperationQueue)會(huì)自動(dòng)管理 operation 的執(zhí)行
// 向隊(duì)列中添加創(chuàng)建的下載任務(wù),之后這個(gè)operation就會(huì)被線程調(diào)度來(lái)執(zhí)行其start方法
[wself.downloadQueue addOperation:operation];
? 如果 operation 執(zhí)行順序是先進(jìn)后出,就設(shè)置 operation 依賴關(guān)系(先加入的依賴于后加入的),并記錄最后一個(gè) operation(lastAddedOperation)
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder)
{
    [wself.lastAddedOperation addDependency:operation];
    wself.lastAddedOperation = operation;
}

b、progressBlock 回調(diào)處理
? 這個(gè) block 有兩個(gè)回調(diào)參數(shù)——接收到的數(shù)據(jù)大小和預(yù)計(jì)數(shù)據(jù)大小
progress:^(NSInteger receivedSize, NSInteger expectedSize)
? 這里用了 weak-strong dance

首先使用 strongSelf 強(qiáng)引用 weakSelf,目的是為了保住 self 不被釋放。然后檢查 self 是否已經(jīng)被釋放(這里為什么先“保活”后“判空”呢?因?yàn)槿绻扰锌盏脑挘锌赡芘锌蘸?self 就被釋放了)。

SDWebImageDownloader *sself = wself;
if (!sself) return;
? 取出 url 對(duì)應(yīng)的回調(diào) block 數(shù)組。這里取的時(shí)候有些講究,考慮了多線程問(wèn)題,而且取的是 copy 的內(nèi)容。
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
    callbacksForURL = [sself.URLCallbacks[url] copy];
});
? 遍歷數(shù)組,從每個(gè)元素(字典)中取出 progressBlock 進(jìn)行回調(diào)
for (NSDictionary *callbacks in callbacksForURL)
{
    SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
    if (callback) callback(receivedSize, expectedSize);
}

c、completedBlock 回調(diào)處理
? 這個(gè) block 有四個(gè)回調(diào)參數(shù)——圖片 UIImage,圖片數(shù)據(jù) NSData,錯(cuò)誤 NSError,是否結(jié)束 isFinished
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) 
? 同樣,這里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
? 接著,取出 url 對(duì)應(yīng)的回調(diào) block 數(shù)組

如果結(jié)束了(isFinished),就移除 url 對(duì)應(yīng)的回調(diào) block 數(shù)組。注意移除的時(shí)候也要考慮多線程問(wèn)題。

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

    if (finished)
    {
        [sself.URLCallbacks removeObjectForKey:url];
    }
});
? 遍歷數(shù)組,從每個(gè)元素(字典)中取出 completedBlock`進(jìn)行回調(diào)
for (NSDictionary *callbacks in callbacksForURL)
{
    SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
    if (callback) callback(image, data, error, finished);
}

d、 cancelBlock 回調(diào)處理
? 同樣,這里也用了 weak-strong dance
SDWebImageDownloader *sself = wself;
if (!sself) return;
? 然后移除 url 對(duì)應(yīng)的所有回調(diào) block
dispatch_barrier_async(sself.barrierQueue, ^{
    [sself.URLCallbacks removeObjectForKey:url];
});

e、前面download方法調(diào)用的方法,返回一個(gè)token
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback
{
    ...
}
? 判斷 url 是否為 nil,如果為 nil 則直接回調(diào) completedBlock 下載完成回調(diào)塊,返回失敗的結(jié)果,然后 return
if (url == nil)
{
    if (completedBlock != nil)
    {
        completedBlock(nil, nil, nil, NO);
    }
    return;
}
? 使用 dispatch_barrier_sync 函數(shù)來(lái)保證同一時(shí)間只有一個(gè)線程能對(duì) URLCallbacks 進(jìn)行操作

這里有個(gè)細(xì)節(jié)需要注意,因?yàn)榭赡芡瑫r(shí)下載多張圖片,所以就可能出現(xiàn)多個(gè)線程同時(shí)訪問(wèn) URLCallbacks 屬性的情況。為了保證線程安全,所以這里使用了 dispatch_barrier_sync 來(lái)分步執(zhí)行添加到 barrierQueue 中的任務(wù),這樣就能保證同一時(shí)間只有一個(gè)線程能對(duì)URLCallbacks 進(jìn)行操作。

dispatch_barrier_sync(self.barrierQueue, ^{
    ...
});
? 如果沒有取到,也就意味著這個(gè) url 是第一次下載,那就初始化一個(gè) callBacksForURL 放到屬性 URLCallbacks 中
// 這是一個(gè)數(shù)組,因?yàn)橐粋€(gè) url 可能不止在一個(gè)地方下載
BOOL first = NO;
if (!self.URLCallbacks[url])
{
    self.URLCallbacks[url] = [NSMutableArray new];
    first = YES;
}
? 從屬性 URLCallbacks(一個(gè)字典) 中取出對(duì)應(yīng) url 的 callBacksForURL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
? 往數(shù)組 callBacksForURL 中添加包裝有 callbacks(progressBlock 和 completedBlock)的字典
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
? 更新 URLCallbacks 存儲(chǔ)的對(duì)應(yīng) url 的 callBacksForURL
self.URLCallbacks[url] = callbacksForURL;
? 處理同一個(gè) URL 的多次下載請(qǐng)求。如果這個(gè)URL是第一次被下載,就要回調(diào) createCallback,createCallback 主要做的就是創(chuàng)建并開啟下載任務(wù)
if (first)
{
    createCallback();
}

URLCallbacks 屬性是一個(gè) NSMutableDictionary 對(duì)象,key 是圖片的 URLvalue 是一個(gè)數(shù)組,包含每個(gè)圖片的多組回調(diào)信息 。用 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 具體的下載任務(wù)

該類繼承自NSOperation,實(shí)現(xiàn)了相關(guān)的自定義操作,所以上層類在使用時(shí)就可以很輕松的用NSOperationQueue來(lái)實(shí)現(xiàn)多線程下載多張圖片。該類邏輯也很簡(jiǎn)單,加入到NSOperationQueue以后,執(zhí)行start方法時(shí)就會(huì)通過(guò)一個(gè)可用的NSURLSession對(duì)象來(lái)創(chuàng)建一個(gè)NSURLSessionDataTask的下載任務(wù),并設(shè)置回調(diào),在回調(diào)方法中接收數(shù)據(jù)并進(jìn)行一系列通知和觸發(fā)回調(diào)塊。

源碼很多地方都用到了SDWebImage自己的編解碼技術(shù),所以又去了解了一下相關(guān)知識(shí)。在展示一張圖片的時(shí)候常使用imageNamed:這樣的類方法去獲取并展示這張圖片,但是圖片是以二進(jìn)制的格式保存在磁盤或內(nèi)存中的,如果要展示一張圖片需要根據(jù)圖片的不同格式去解碼為正確的位圖交由系統(tǒng)控件來(lái)展示,而解碼的操作默認(rèn)是放在主線程執(zhí)行。凡是放在主線程執(zhí)行的任務(wù)都務(wù)必需要考慮清楚,如果有大量圖片要展示,就會(huì)在主線程中執(zhí)行大量的解碼任務(wù),勢(shì)必會(huì)阻塞主線程造成卡頓,所以SDWebImage自己實(shí)現(xiàn)相關(guān)的編解碼操作,并在子線程中處理,就不會(huì)影響主線程的相關(guān)操作。

對(duì)于同步代碼塊有點(diǎn)不解,望理解的讀者周知。SDWebImage下載的邏輯也挺簡(jiǎn)單的,本類SDWebImageDownloaderOperationNSOperation的子類,所以可以使用NSOperationQueue來(lái)實(shí)現(xiàn)多線程下載。但是每一個(gè)Operation類對(duì)應(yīng)一個(gè)NSURLSessionTask的下載任務(wù),也就是說(shuō),SDWebImageDownloader類在需要下載圖片的時(shí)候就創(chuàng)建一個(gè)Operation, 然后將這個(gè)Operation加入到OperationQueue中,就會(huì)執(zhí)行start方法,start方法會(huì)創(chuàng)建一個(gè)Task來(lái)實(shí)現(xiàn)下載。所以整個(gè)下載任務(wù)有兩個(gè)子線程,一個(gè)是Operation執(zhí)行start方法的線程用來(lái)開啟Task的下載任務(wù),一個(gè)是Task的線程來(lái)執(zhí)行下載任務(wù)。OperationTask是一對(duì)一的關(guān)系,應(yīng)該不會(huì)有競(jìng)爭(zhēng)條件產(chǎn)生呀?


1、屬性與方法聲明

a、全局變量
// 進(jìn)度回調(diào)塊和下載完成回調(diào)塊的字符串類型的key
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

// 定義了一個(gè)可變字典類型的回調(diào)塊集合,這個(gè)字典key的取值就是上面兩個(gè)字符串,value就是回調(diào)塊了
typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

b、下載任務(wù)協(xié)議
開發(fā)者可以實(shí)現(xiàn)自己的下載操作只需要實(shí)現(xiàn)該協(xié)議即可
@protocol SDWebImageDownloaderOperation <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

// 初始化函數(shù),根據(jù)指定的request、session和下載選項(xiàng)創(chuàng)建一個(gè)下載任務(wù)
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

// 添加進(jìn)度和完成后的回調(diào)塊
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

@end
SDWebImageDownloaderOperation類繼承自NSOperation并遵守了SDWebImageDownloaderOperation協(xié)議
// 該類繼承自NSOperation主要是為了將任務(wù)加進(jìn)并發(fā)隊(duì)列里實(shí)現(xiàn)多線程下載多張圖片
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperation>

c、公開的屬性
// 下載任務(wù)的request
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;

// 連接服務(wù)端后的收到的響應(yīng)
@property (strong, nonatomic, readonly, nullable) NSURLResponse *response;

// 執(zhí)行下載操作的下載任務(wù)
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;

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

// 下載時(shí)配置的相關(guān)內(nèi)容
@property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options;

d、公開的方法
// 初始化方法需要下載文件的request、session以及下載相關(guān)配置選項(xiàng)
// 真正實(shí)現(xiàn)下載操作的是NSURLSessionTask類的子類,這里就可以看出SDWebImage使用NSURLSession實(shí)現(xiàn)下載圖片的功能
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

// 添加一個(gè)進(jìn)度回調(diào)塊和下載完成后的回調(diào)塊
// 返回一個(gè)token,用于取消這個(gè)下載任務(wù),這個(gè)token其實(shí)是一個(gè)字典
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

// 這個(gè)方法不是用來(lái)取消下載任務(wù)的,而是刪除前一個(gè)方法添加的進(jìn)度回調(diào)塊和下載完成回調(diào)塊,當(dāng)所有的回調(diào)塊都刪除后,下載任務(wù)也會(huì)被取消
// 需要傳入上一個(gè)方法返回的token
- (BOOL)cancel:(nullable id)token;

e、私有屬性
回調(diào)塊數(shù)組,數(shù)組內(nèi)的元素即為前面自定義的字典
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
繼承NSOperation需要定義executing和finished屬性,并實(shí)現(xiàn)getter和setter,手動(dòng)觸發(fā)KVO通知
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
圖片數(shù)據(jù)
// 可變NSData數(shù)據(jù),存儲(chǔ)下載的圖片數(shù)據(jù)
@property (strong, nonatomic, nullable) NSMutableData *imageData;
// 緩存的圖片數(shù)據(jù)
@property (copy, nonatomic, nullable) NSData *cachedData;
// 需要下載的文件的大小
@property (assign, nonatomic) NSUInteger expectedSize;
// 接收到下載的文件的大小
@property (assign, nonatomic) NSUInteger receivedSize;
// 上一進(jìn)度百分比
@property (assign, nonatomic) double previousProgress;
連接服務(wù)端后的收到的響應(yīng)
@property (strong, nonatomic, nullable, readwrite) NSURLResponse *response;
@property (strong, nonatomic, nullable) NSError *responseError;
// 修改原始URL響應(yīng)
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
NSURLSession屬性
/*
這里是weak修飾的NSURLSession屬性
作者解釋到unownedSession有可能不可用,因?yàn)檫@個(gè)session是外面?zhèn)鬟M(jìn)來(lái)的,由其他類負(fù)責(zé)管理這個(gè)session,本類不負(fù)責(zé)管理
這個(gè)session有可能會(huì)被回收,當(dāng)不可用時(shí)使用下面那個(gè)session
*/
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;

/*
 strong修飾的session,當(dāng)上面weak的session不可用時(shí),需要?jiǎng)?chuàng)建一個(gè)session
 這個(gè)session需要由本類負(fù)責(zé)管理,需要在合適的地方調(diào)用invalid方法打破引用循環(huán)
 */
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
圖像解碼
// 圖像解碼的串行操作隊(duì)列
@property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue;
// 解密圖像數(shù)據(jù)
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor;
下載任務(wù)
// 具體的下載任務(wù)
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;
// iOS上支持在后臺(tái)下載時(shí)需要一個(gè)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,所以這個(gè)session是外部傳入的,本類就不需要負(fù)責(zé)管理它
        // 但是它有可能會(huì)被釋放,所以當(dāng)這個(gè)session不可用時(shí)需要自己創(chuàng)建一個(gè)新的session并自行管理
        _unownedSession = session;
        _coderQueue = [NSOperationQueue new];
        _coderQueue.maxConcurrentOperationCount = 1;
        _backgroundTaskId = UIBackgroundTaskInvalid;
    }
    return self;
}

2、回調(diào)塊

a、添加進(jìn)度回調(diào)塊和下載完成回調(diào)塊

往一個(gè)字典類型的數(shù)組中添加回調(diào)塊,這個(gè)字典最多只有兩個(gè)key-value鍵值對(duì),數(shù)組中可以有多個(gè)這樣的字典,每添加一個(gè)進(jìn)度回調(diào)塊和下載完成回調(diào)塊就會(huì)把這個(gè)字典返回作為token。

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
{
    // 創(chuàng)建一個(gè)<NSString,id>類型的可變字典,value為回調(diào)塊
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    // 如果進(jìn)度回調(diào)塊存在就加進(jìn)字典里,key為@"progress"
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    // 如果下載完成回調(diào)塊存在就加進(jìn)字典里,key為@"completed"
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    // 阻塞并發(fā)隊(duì)列,串行執(zhí)行添加進(jìn)數(shù)組的操作
    @synchronized (self)
    {
        [self.callbackBlocks addObject:callbacks];
    }
    // 回的token其實(shí)就是這個(gè)字典
    return callbacks;
}

b、通過(guò)key獲取回調(diào)塊數(shù)組中所有對(duì)應(yīng)key的回調(diào)塊
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key
{
    NSMutableArray<id> *callbacks;
    // 同步方式執(zhí)行,阻塞當(dāng)前線程也阻塞隊(duì)列
    @synchronized (self)
    {
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
    }
    // 如果字典中沒有對(duì)應(yīng)key會(huì)返回null,所以需要?jiǎng)h除為null的元素
    [callbacks removeObjectIdenticalTo:[NSNull null]];
    return [callbacks copy];
}

c、取消方法

在取消任務(wù)方法中會(huì)從數(shù)組中刪除掉這個(gè)字典,但是只有當(dāng)數(shù)組中的回調(diào)塊字典全部被刪除完了才會(huì)真正取消任務(wù)。

- (BOOL)cancel:(nullable id)token
{
    if (!token) return NO;
    
    BOOL shouldCancel = NO;
    // 同步方式執(zhí)行,阻塞當(dāng)前線程也阻塞隊(duì)列
    @synchronized (self)
    {
        // 根據(jù)token刪除數(shù)組中的數(shù)據(jù),token就是key為string,value為block的字典
        NSMutableArray *tempCallbackBlocks = [self.callbackBlocks mutableCopy];
        // 刪除的就是數(shù)組中的字典元素
        [tempCallbackBlocks removeObjectIdenticalTo:token];
        // 如果回調(diào)塊數(shù)組長(zhǎng)度為0就真的要取消下載任務(wù)了,因?yàn)橐呀?jīng)沒有人來(lái)接收下載完成和下載進(jìn)度的信息,下載完成也沒有任何意義
        if (tempCallbackBlocks.count == 0)
        {
            shouldCancel = YES;
        }
    }
    
    // 如果要真的要取消任務(wù)就調(diào)用cancel方法
    if (shouldCancel)
    {
        [self cancel];
    }
    
    return shouldCancel;
}

3、啟動(dòng)下載任務(wù)

重寫NSOperation類的start方法,任務(wù)添加到NSOperationQueue后會(huì)執(zhí)行該方法,啟動(dòng)下載任務(wù)。判斷session是否可用然后決定是否要自行管理一個(gè)NSURLSession對(duì)象,接下來(lái)就使用這個(gè)session創(chuàng)建一個(gè)NSURLSessionDataTask對(duì)象,這個(gè)對(duì)象是真正執(zhí)行下載和服務(wù)端交互的對(duì)象,接下來(lái)就開啟這個(gè)下載任務(wù)然后進(jìn)行通知和回調(diào)塊的觸發(fā)工作。

- (void)start
{
    ...
}
a、同步代碼塊,防止產(chǎn)生競(jìng)爭(zhēng)條件

NSOperation子類加進(jìn)NSOperationQueue后會(huì)自行調(diào)用start方法,并且只會(huì)執(zhí)行一次,不太理解為什么需要加這個(gè)。

@synchronized (self)
{
    ...
}
? 判斷是否取消了下載任務(wù)
if (self.isCancelled)
{
    // 如果取消了就設(shè)置finished為YES,
    self.finished = YES;
    // 用戶取消錯(cuò)誤
    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]];
    // 調(diào)用reset方法
    [self reset];
    return;
}
? iOS支持可以在app進(jìn)入后臺(tái)后繼續(xù)下載
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,為空則自行創(chuàng)建一個(gè)NSURLSession對(duì)象
NSURLSession *session = self.unownedSession;
if (!session)
{
    // 為空則自行創(chuàng)建一個(gè)NSURLSession對(duì)象
    // session運(yùn)行在默認(rèn)模式下
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    // 超時(shí)時(shí)間15s
    sessionConfig.timeoutIntervalForRequest = 15;
    
    // delegateQueue為nil,所以回調(diào)方法默認(rèn)在一個(gè)子線程的串行隊(duì)列中執(zhí)行
    session = [NSURLSession sessionWithConfiguration:sessionConfig
                                            delegate:self
                                       delegateQueue:nil];
    // 局部變量賦值
    self.ownedSession = session;
}
? 根據(jù)配置的下載選項(xiàng)獲取網(wǎng)絡(luò)請(qǐng)求的緩存數(shù)據(jù)
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來(lái)創(chuàng)建一個(gè)NSURLSessionDataTask類型的下載任務(wù)
self.dataTask = [session dataTaskWithRequest:self.request];
? 設(shè)置NSOperation子類的executing屬性,標(biāo)識(shí)開始下載任務(wù)
self.executing = YES;

b、開始執(zhí)行任務(wù)
? 如果這個(gè)NSURLSessionDataTask不為空即開啟成功
if (self.dataTask)
{
    ...
}
? 設(shè)置任務(wù)執(zhí)行優(yōu)先級(jí)
if (self.options & SDWebImageDownloaderHighPriority)
{
    // 設(shè)置任務(wù)優(yōu)先級(jí)為高優(yōu)先級(jí)
    self.dataTask.priority = NSURLSessionTaskPriorityHigh;
    // 圖像解碼的串行操作隊(duì)列的服務(wù)質(zhì)量為用戶交互
    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任務(wù)開始執(zhí)行
[self.dataTask resume];
? 遍歷所有的進(jìn)度回調(diào)塊并執(zhí)行
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
{
    progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
? 在主線程中發(fā)送通知,并將self傳出去
__block typeof(self) strongSelf = self;
// 在什么線程發(fā)送通知,就會(huì)在什么線程接收通知
// 為了防止其他監(jiān)聽通知的對(duì)象在回調(diào)方法中修改UI,這里就需要在主線程中發(fā)送通知
dispatch_async(dispatch_get_main_queue(), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});

4、設(shè)置取消與完成下載任務(wù)

a、取消下載任務(wù)
? SDWebImageOperation協(xié)議的cancel方法,取消任務(wù),調(diào)用cancelInternal方法
- (void)cancel
{
    @synchronized (self)
    {
        // 真正取消下載任務(wù)的方法
        [self cancelInternal];
    }
}
? 真正取消下載任務(wù)的方法
- (void)cancelInternal
{
    // 如果下載任務(wù)已經(jīng)結(jié)束了直接返回
    if (self.isFinished) return;
    
    // 調(diào)用NSOperation類的cancel方法,即將isCancelled屬性置為YES
    [super cancel];

    // 如果NSURLSessionDataTask下載圖片的任務(wù)存在
    if (self.dataTask)
    {
        // 調(diào)用其cancel方法取消下載任務(wù)
        [self.dataTask cancel];
        
        // 在主線程中發(fā)出下載停止的通知
        __block typeof(self) strongSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
        });
        
        // 設(shè)置兩個(gè)屬性的值
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    // 調(diào)用reset方法
    [self reset];
}

b、完成下載任務(wù)
? 下載完成后調(diào)用的方法
- (void)done
{
    // 設(shè)置finished為YES executing為NO
    self.finished = YES;
    self.executing = NO;
    
    // 調(diào)用reset方法
    [self reset];
}
? NSOperation子類finished屬性的setter:手動(dòng)觸發(fā)KVO通知
- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}
? NSOperation子類isExecuting屬性的setter:手動(dòng)觸發(fā)KVO通知
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
? 重寫NSOperation方法,標(biāo)識(shí)這是一個(gè)并發(fā)任務(wù)
- (BOOL)isConcurrent
{
    return YES;
}

c、大俠請(qǐng)重新來(lái)過(guò)
- (void)reset
{
    @synchronized (self)
    {
        // 刪除回調(diào)塊字典數(shù)組的所有元素
        [self.callbackBlocks removeAllObjects];
        // NSURLSessionDataTask對(duì)象置為nil
        self.dataTask = nil;
        
        // 如果ownedSession存在,就需要我們手動(dòng)調(diào)用invalidateAndCancel方法打破引用循環(huán)
        if (self.ownedSession)
        {
            [self.ownedSession invalidateAndCancel];
            self.ownedSession = nil;
        }
        
        // 停止后臺(tái)下載
        if (self.backgroundTaskId != UIBackgroundTaskInvalid)
        {
            UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
            [app endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    }
}

5、NSURLSessionDataDelegate 代理方法

下面幾個(gè)方法就是在接收到服務(wù)端響應(yīng)后進(jìn)行一個(gè)處理,判斷是否是正常響應(yīng),如果是正常響應(yīng)就進(jìn)行各種賦值和初始化操作,并觸發(fā)回調(diào)塊,進(jìn)行通知等操作,如果不是正常響應(yīng)就結(jié)束下載任務(wù)。接下來(lái)的一個(gè)比較重要的方法就是接收到圖片數(shù)據(jù)的處理,接收到數(shù)據(jù)后就追加到可變數(shù)據(jù)中,如果需要在圖片沒有下載完成時(shí)就展示部分圖片,需要進(jìn)行一個(gè)解碼的操作然后調(diào)用回調(diào)塊將圖片數(shù)據(jù)回傳,接著就會(huì)調(diào)用存儲(chǔ)的進(jìn)度回調(diào)塊來(lái)通知現(xiàn)在的下載進(jìn)度,回傳圖片的總長(zhǎng)度和已經(jīng)下載長(zhǎng)度的信息。

a、收到服務(wù)端響應(yīng),在一次請(qǐng)求中只會(huì)執(zhí)行一次
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
    ...
}
? 修改原始URL響應(yīng)
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"}];
    }
}
// 將連接服務(wù)端后的收到的響應(yīng)賦值到成員變量
self.response = response;
? 根據(jù)http狀態(tài)碼判斷是否成功響應(yīng),如果響應(yīng)不正常觸發(fā)異?;卣{(diào)塊。需要注意的是304被認(rèn)為是異常響應(yīng)
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"}];
}
? 如果響應(yīng)正常遍歷進(jìn)度回調(diào)塊并觸發(fā)進(jìn)度回調(diào)塊
// 獲取要下載圖片的長(zhǎng)度
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
// 設(shè)置長(zhǎng)度
self.expectedSize = expected;

// 如果響應(yīng)正常
if (valid)
{
    // 遍歷進(jìn)度回調(diào)塊并觸發(fā)進(jìn)度回調(diào)塊
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
    {
        progressBlock(0, expected, self.request.URL);
    }
}
? 如果響應(yīng)不正常則直接取消下載任務(wù)
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;

if (valid)
{
    ...
}
else
{
    disposition = NSURLSessionResponseCancel;
}
? 如果有回調(diào)塊就執(zhí)行
if (completionHandler)
{
    completionHandler(disposition);
}

b、收到數(shù)據(jù)的回調(diào)方法,可能執(zhí)行多次
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    ...
}
? 向可變數(shù)據(jù)中添加接收到的數(shù)據(jù)
if (!self.imageData)
{
    self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
[self.imageData appendData:data];
? 計(jì)算下載進(jìn)度
// 獲取已經(jīng)下載了多大的數(shù)據(jù)
self.receivedSize = self.imageData.length;
// 判斷是否已經(jīng)下載完成
BOOL finished = (self.receivedSize >= self.expectedSize);
// 計(jì)算下載進(jìn)度
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;
? 漸進(jìn)式解碼
// 使用數(shù)據(jù)解密將禁用漸進(jìn)式解碼
BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
// 支持漸進(jìn)式解碼
if (supportProgressive)
{
    // 獲取圖像數(shù)據(jù)
    NSData *imageData = [self.imageData copy];
    
    // 下載期間最多保留一個(gè)按照下載進(jìn)度進(jìn)行解碼的操作
    // coderQueue是圖像解碼的串行操作隊(duì)列
    if (self.coderQueue.operationCount == 0)
    {
        // NSOperation有自動(dòng)釋放池,不需要額外創(chuàng)建一個(gè)
        [self.coderQueue addOperationWithBlock:^{
            // 將數(shù)據(jù)交給解碼器返回一個(gè)圖片
            UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
            
            if (image)
            {
                // 觸發(fā)回調(diào)塊回傳這個(gè)圖片
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }];
    }
}
? 調(diào)用進(jìn)度回調(diào)塊并觸發(fā)進(jìn)度回調(diào)塊
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
{
    progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}

c、如果要緩存響應(yīng)時(shí)回調(diào)該方法
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    NSCachedURLResponse *cachedResponse = proposedResponse;

    // 如果request的緩存策略是不緩存本地?cái)?shù)據(jù)就設(shè)置為nil
    if (!(self.options & SDWebImageDownloaderUseNSURLCache))
    {
        // 防止緩存響應(yīng),避免進(jìn)行本地緩存
        cachedResponse = nil;
    }
    
    // 調(diào)用回調(diào)塊
    if (completionHandler)
    {
        completionHandler(cachedResponse);
    }
}

6、NSURLSessionTaskDelegate 代理方法

a、下載完成或下載失敗時(shí)的回調(diào)方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    ...
}
? 主線程根據(jù)error是否為空發(fā)送對(duì)應(yīng)通知
@synchronized(self)
{
    // 置空
    self.dataTask = nil;
    
    // 主線程根據(jù)error是否為空發(fā)送對(duì)應(yīng)通知
    __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存在,即下載過(guò)程中出錯(cuò)
// 自定義錯(cuò)誤而不是URLSession錯(cuò)誤
if (self.responseError)
{
    error = self.responseError;
}
// 觸發(fā)對(duì)應(yīng)回調(diào)塊
[self callCompletionBlocksWithError:error];
// 下載完成后調(diào)用的方法
[self done];
? 下載成功則對(duì)圖片進(jìn)行解碼
// 判斷下載完成回調(diào)塊個(gè)數(shù)是否大于0
if ([self callbacksForKey:kCompletedCallbackKey].count > 0)
{
    // 獲取不可變data圖片數(shù)據(jù)
    NSData *imageData = [self.imageData copy];
    self.imageData = nil;
    // 如果下載的圖片和解密圖像數(shù)據(jù)的解碼器存在
    if (imageData && self.decryptor)
    {
        // 解碼圖片,返回data
        imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
    }
    ...
}
? 如果下載設(shè)置為只使用緩存數(shù)據(jù)就會(huì)判斷緩存數(shù)據(jù)與當(dāng)前獲取的數(shù)據(jù)是否一致,一致就觸發(fā)完成回調(diào)塊
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData])
{
    // 錯(cuò)誤:下載的圖像不會(huì)被修改和忽略
    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}];
    // 調(diào)用帶有未修改錯(cuò)誤的回調(diào)完成塊
    [self callCompletionBlocksWithError:self.responseError];
    [self done];
}
? 解碼圖片,返回圖片
// 取消之前的所有解碼過(guò)程
[self.coderQueue cancelAllOperations];

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

    CGSize imageSize = image.size;
    // 下載的圖像有0個(gè)像素
    if (imageSize.width == 0 || imageSize.height == 0)
    {
        // 調(diào)用帶有圖像大小為0錯(cuò)誤的回調(diào)完成塊
        NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
    }
    else
    {
        // 觸發(fā)成功完成回調(diào)塊
        [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
    }
    // 下載完成后調(diào)用的方法
    [self done];
}];

b、如果是https訪問(wèn)就需要設(shè)置SSL證書相關(guān)
- (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、遍歷所有的完成回調(diào)塊,在主線程中觸發(fā)
- (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);
        }
    });
}

續(xù)文見下篇:IOS源碼解析:SDWeblmage(下)


Demo

Demo在我的Github上,歡迎下載。
SourceCodeAnalysisDemo

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容