IOS源碼解析:SDWeblmage (上)

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

目錄

  • 一、系統提供的NSURLSession的使用
    • 1、簡介
    • 2、Get請求
    • 3、發(fā)送POST請求
    • 4、下載文件
    • 5、監(jiān)聽具體請求狀態(tài)的代理
  • 二、認識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的子類,主要用于進行一些比較簡短數據的獲取,通常用于發(fā)送GET/POST請求。默認發(fā)起GET請求,如果需要發(fā)起POST請求需要額外的操作。創(chuàng)建的任務默認是掛起狀態(tài)的,所以為了啟動網絡請求,調用其resume方法即可開始執(zhí)行請求。當任務完成時就會執(zhí)行上述回調塊,當然也可以使用代理的方式監(jiān)聽網絡請求。這樣看來它的使用真的很方便,并且默認會自動開啟多線程異步執(zhí)行。下面栗子的回調塊中輸出了當前線程可以看出并不是主線程,所以在回調中如果要進行UI的更新操作需要放到主線程中執(zhí)行。

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

NSURLSessionTask類似抽象類不提供網絡請求的功能,具體實現由其子類實現。上面的栗子使用的就是NSURLSessionDataTask主要用來獲取一些簡短的數據,如發(fā)起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、發(fā)送POST請求

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

// 創(chuàng)建一個可變的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) {
    // 請求完成后會執(zhí)行回調塊,可以根據服務端返回的數據轉換為JSON數據或者HTML等格式
    NSLog(@"數據:%@,錯誤:%@,線程:%@", data, error, [NSThread currentThread]);
    NSLog(@"回應:%@", response);
}];

// 啟動任務
[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對象
NSURLSession *session = [NSURLSession sharedSession];

// 創(chuàng)建一個下載任務
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、監(jiān)聽具體請求狀態(tài)的代理

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

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

@interface ViewController ()<NSURLSessionDelegate>

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

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

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

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

c、從服務端收到數據,一次請求中可能執(zhí)行多次
- (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、輸出結果

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

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的圖片下載分類,只要一行代碼就可以實現圖片異步下載和緩存功能。這樣開發(fā)者就無須花太多精力在圖片下載細節(jié)上,專心處理業(yè)務邏輯。


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 中的一部分可能是動態(tài)變化的(比如獲取權限上的限制),所以我們只需要把 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:預下載圖片,方便后續(xù)使用
Categories:分類
  • UIView+WebCacheOperation:用來記錄圖片加載的 operation,方便需要時取消和移除圖片加載的 operation
  • UIImageView+WebCache:集成 SDWebImageManager 的圖片下載和緩存功能到 UIImageView 的方法中,方便調用
  • UIImageView+HighlightedWebCache:也是包裝了 SDWebImageManager,只不過是用于加載 highlighted 狀態(tài)的圖片
  • 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 退到后臺時還能繼續(xù)執(zhí)行任務,不再執(zhí)行后臺任務時,需要調用 -[UIApplication endBackgroundTask:] 方法標記后臺任務結束。

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

默認有效期:

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

默認最大緩存空間:

maxCacheSize = <#unlimited#>
MKAnnotationView

MKAnnotationView 是屬于 MapKit 框架的一個類,繼承自UIView,是用來展示地圖上的annotation 信息的,它有一個用來設置圖片的屬性 image

假如自己來實現一個圖片下載工具,該怎么寫?

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


四、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, //低優(yōu)先級
    SDWebImageDownloaderProgressiveDownload = 1 << 1, //帶有下載進度
    SDWebImageDownloaderUseNSURLCache = 1 << 2, //使用緩存
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, //忽略緩存響應
    SDWebImageDownloaderContinueInBackground = 1 << 4, //支持后臺下載
    SDWebImageDownloaderHandleCookies = 1 << 5, //使用Cookies
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, //允許驗證SSL
   SDWebImageDownloaderHighPriority = 1 << 7, //高優(yōu)先級
};

下載選項枚舉使用了位運算。通過“與”運算符,可以判斷是否設置了某個枚舉選項,因為每個枚舉選擇項中只有一位是1,其余位都是 0,所以只有參與運算的另一個二進制值在同樣的位置上也為 1,與運算的結果才不會為 0。

0101 (相當于 SDWebImageDownloaderLowPriority | SDWebImageDownloaderUseNSURLCache)
& 0100 (= 1 << 2,也就是 SDWebImageDownloaderUseNSURLCache)
= 0100 (> 0,也就意味著 option 參數中設置了 SDWebImageDownloaderUseNSURLCache)
下載任務執(zhí)行順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) 
{
    SDWebImageDownloaderFIFOExecutionOrder, //執(zhí)行順序為先進先出
    SDWebImageDownloaderLIFOExecutionOrder //執(zhí)行順序為后進先出
};
回調塊
// 進度回調塊
typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock;
// 下載完成的回調塊
typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock;

b、屬性
公開屬性
@property (assign, nonatomic) BOOL shouldDecompressImages; //下載完成后是否需要解壓縮圖片,默認為 YES
@property (assign, nonatomic) NSInteger maxConcurrentDownloads; //支持的最大同時下載圖片的數量,其實就是NSOperationQueue支持的最大并發(fā)數
@property (readonly, nonatomic) NSUInteger currentDownloadCount; //當前正在下載圖片的數量,其實就是NSOperationQueue的operationCount,即正在執(zhí)行下載任務的operation的數量
@property (assign, nonatomic) NSTimeInterval downloadTimeout; //下載時連接服務器的超時時間,默認15s
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //執(zhí)行下載任務的順序,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,開發(fā)者可以實現相關協議進行自定義
- (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:類方法,類加載的時候執(zhí)行
+ (void)initialize
{
    ...
}
? 如果導入了SDNetworkActivityIndicator文件,就會展示一個小菊花

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

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

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

[[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 的默認執(zhí)行順序為FIFO(先進先出還是先進后出)
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
? 初始化 _downloadQueue(下載隊列)并設置最大并發(fā)數為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(創(chuàng)建一個GCD并發(fā)隊列)
_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最大并發(fā)數
- (void)setMaxConcurrentDownloads:(NSInteger)maxConcurrentDownloads
{
    _downloadQueue.maxConcurrentOperationCount = maxConcurrentDownloads;
}
獲取最大同時下載圖片的數量
- (NSInteger)maxConcurrentDownloads
{
    return _downloadQueue.maxConcurrentOperationCount;
}
當前正在下載圖片數量,即NSOperationQueue中正在執(zhí)行的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中為了防止引用循環(huán)和空指針,先weak后strong
    __weak __typeof(self)wself = self;
    ...
    // 返回 createCallback 中創(chuàng)建的 operation(SDWebImageDownloaderOperation)
    return operation;
}
a、直接調用另一個方法:addProgressCallback
// 把入參 url、progressBlock 和 completedBlock 傳進該方法創(chuàng)建一個SDWebImageDownloaderOperation類的對象,并在第一次下載該 URL 時回調 createCallback。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
    ...
}];
? 設置超時時間
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0)
{
    timeoutInterval = 15.0;
}
? 創(chuàng)建一個可變的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;
}
? 傳入網絡請求和下載選項配置創(chuàng)建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 的隊列優(yōu)先級
if (options & SDWebImageDownloaderHighPriority)
{
    operation.queuePriority = NSOperationQueuePriorityHigh;
}
else if (options & SDWebImageDownloaderLowPriority)
{
    operation.queuePriority = NSOperationQueuePriorityLow;
}
? 將 operation 加入到隊列 downloadQueue 中,隊列(NSOperationQueue)會自動管理 operation 的執(zhí)行
// 向隊列中添加創(chuàng)建的下載任務,之后這個operation就會被線程調度來執(zhí)行其start方法
[wself.downloadQueue addOperation:operation];
? 如果 operation 執(zhí)行順序是先進后出,就設置 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 進行操作

這里有個細節(jié)需要注意,因為可能同時下載多張圖片,所以就可能出現多個線程同時訪問 URLCallbacks 屬性的情況。為了保證線程安全,所以這里使用了 dispatch_barrier_sync 來分步執(zhí)行添加到 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 主要做的就是創(chuàng)建并開啟下載任務
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以后,執(zhí)行start方法時就會通過一個可用的NSURLSession對象來創(chuàng)建一個NSURLSessionDataTask的下載任務,并設置回調,在回調方法中接收數據并進行一系列通知和觸發(fā)回調塊。

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

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


1、屬性與方法聲明

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

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

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

// 初始化函數,根據指定的request、session和下載選項創(chuàng)建一個下載任務
- (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主要是為了將任務加進并發(fā)隊列里實現多線程下載多張圖片
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperation>

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

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

// 執(zhí)行下載操作的下載任務
@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,手動觸發(fā)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是外面?zhèn)鬟M來的,由其他類負責管理這個session,本類不負責管理
這個session有可能會被回收,當不可用時使用下面那個session
*/
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;

/*
 strong修飾的session,當上面weak的session不可用時,需要創(chuàng)建一個session
 這個session需要由本類負責管理,需要在合適的地方調用invalid方法打破引用循環(huán)
 */
@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不可用時需要自己創(chuàng)建一個新的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
{
    // 創(chuàng)建一個<NSString,id>類型的可變字典,value為回調塊
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    // 如果進度回調塊存在就加進字典里,key為@"progress"
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    // 如果下載完成回調塊存在就加進字典里,key為@"completed"
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    // 阻塞并發(fā)隊列,串行執(zhí)行添加進數組的操作
    @synchronized (self)
    {
        [self.callbackBlocks addObject:callbacks];
    }
    // 回的token其實就是這個字典
    return callbacks;
}

b、通過key獲取回調塊數組中所有對應key的回調塊
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key
{
    NSMutableArray<id> *callbacks;
    // 同步方式執(zhí)行,阻塞當前線程也阻塞隊列
    @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;
    // 同步方式執(zhí)行,阻塞當前線程也阻塞隊列
    @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后會執(zhí)行該方法,啟動下載任務。判斷session是否可用然后決定是否要自行管理一個NSURLSession對象,接下來就使用這個session創(chuàng)建一個NSURLSessionDataTask對象,這個對象是真正執(zhí)行下載和服務端交互的對象,接下來就開啟這個下載任務然后進行通知和回調塊的觸發(fā)工作。

- (void)start
{
    ...
}
a、同步代碼塊,防止產生競爭條件

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

@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進入后臺后繼續(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)建一個NSURLSession對象
NSURLSession *session = self.unownedSession;
if (!session)
{
    // 為空則自行創(chuàng)建一個NSURLSession對象
    // session運行在默認模式下
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    // 超時時間15s
    sessionConfig.timeoutIntervalForRequest = 15;
    
    // delegateQueue為nil,所以回調方法默認在一個子線程的串行隊列中執(zhí)行
    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來創(chuàng)建一個NSURLSessionDataTask類型的下載任務
self.dataTask = [session dataTaskWithRequest:self.request];
? 設置NSOperation子類的executing屬性,標識開始下載任務
self.executing = YES;

b、開始執(zhí)行任務
? 如果這個NSURLSessionDataTask不為空即開啟成功
if (self.dataTask)
{
    ...
}
? 設置任務執(zhí)行優(yōu)先級
if (self.options & SDWebImageDownloaderHighPriority)
{
    // 設置任務優(yōu)先級為高優(yōu)先級
    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任務開始執(zhí)行
[self.dataTask resume];
? 遍歷所有的進度回調塊并執(zhí)行
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey])
{
    progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
? 在主線程中發(fā)送通知,并將self傳出去
__block typeof(self) strongSelf = self;
// 在什么線程發(fā)送通知,就會在什么線程接收通知
// 為了防止其他監(jiān)聽通知的對象在回調方法中修改UI,這里就需要在主線程中發(fā)送通知
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];
        
        // 在主線程中發(fā)出下載停止的通知
        __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:手動觸發(fā)KVO通知
- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}
? NSOperation子類isExecuting屬性的setter:手動觸發(fā)KVO通知
- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
? 重寫NSOperation方法,標識這是一個并發(fā)任務
- (BOOL)isConcurrent
{
    return YES;
}

c、大俠請重新來過
- (void)reset
{
    @synchronized (self)
    {
        // 刪除回調塊字典數組的所有元素
        [self.callbackBlocks removeAllObjects];
        // NSURLSessionDataTask對象置為nil
        self.dataTask = nil;
        
        // 如果ownedSession存在,就需要我們手動調用invalidateAndCancel方法打破引用循環(huán)
        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 代理方法

下面幾個方法就是在接收到服務端響應后進行一個處理,判斷是否是正常響應,如果是正常響應就進行各種賦值和初始化操作,并觸發(fā)回調塊,進行通知等操作,如果不是正常響應就結束下載任務。接下來的一個比較重要的方法就是接收到圖片數據的處理,接收到數據后就追加到可變數據中,如果需要在圖片沒有下載完成時就展示部分圖片,需要進行一個解碼的操作然后調用回調塊將圖片數據回傳,接著就會調用存儲的進度回調塊來通知現在的下載進度,回傳圖片的總長度和已經下載長度的信息。

a、收到服務端響應,在一次請求中只會執(zhí)行一次
- (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狀態(tài)碼判斷是否成功響應,如果響應不正常觸發(fā)異常回調塊。需要注意的是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"}];
}
? 如果響應正常遍歷進度回調塊并觸發(fā)進度回調塊
// 獲取要下載圖片的長度
NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
// 設置長度
self.expectedSize = expected;

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

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

b、收到數據的回調方法,可能執(zhí)行多次
- (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有自動釋放池,不需要額外創(chuàng)建一個
        [self.coderQueue addOperationWithBlock:^{
            // 將數據交給解碼器返回一個圖片
            UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
            
            if (image)
            {
                // 觸發(fā)回調塊回傳這個圖片
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }];
    }
}
? 調用進度回調塊并觸發(fā)進度回調塊
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是否為空發(fā)送對應通知
@synchronized(self)
{
    // 置空
    self.dataTask = nil;
    
    // 主線程根據error是否為空發(fā)送對應通知
    __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;
}
// 觸發(fā)對應回調塊
[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];
    }
    ...
}
? 如果下載設置為只使用緩存數據就會判斷緩存數據與當前獲取的數據是否一致,一致就觸發(fā)完成回調塊
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
    {
        // 觸發(fā)成功完成回調塊
        [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、遍歷所有的完成回調塊,在主線程中觸發(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

參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容