最近真的真的太太太忙了,都是抽空寫的,各種事情,html,iOS客戶端升級,炸了
上一篇我們基本上看完了SDWebImage
整個工作流程,下面我們具體看一下快取下載圖片中涉及到的相關的類
SDWebImageDownloader
SDWebImageManager
實現下載依賴於下載器:SDWebImageDownloader
,下載器負責管理下載任務,而執行下載任務是由SDWebImageDownloaderOperation
操作完成
SDWebImageManager
實現下載 就是呼叫下面這個方法:
1 2 3 4 |
- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock |
我們還是先來看看SDWebImageDownloader
裡面都寫了些什麼
SDWebImageDownloader.h
1 2 3 4 |
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { //這個屬於預設的使用模式了,前往下載,返回進度block資訊,完成時呼叫completedBlock SDWebImageDownloaderLowPriority = 1 |
這些選項主要涉及到下載的優先順序,快取,後臺任務執行,cookie處理以及證書認證幾個方面,在建立下載操作的時候可以使用組合的選項來完成一些特殊的需求
定義裡兩個常量,後面通知的時候用的,這裡的常量是全域性常量
全域性常量:不管你定義在任何資料夾,外部都能訪問
1 |
const NSString *myName = @"楊千嬅染了紅頭髮"; |
區域性常量:用static修飾後,不能提供外界訪問(只能在賦值的.m檔案使用,外界不可訪問)
1 |
static const NSString *myName= @"楊千嬅染了紅頭髮"; |
1 2 3 |
//官方也更推薦這樣定義常量 而不是用#define extern NSString *const SDWebImageDownloadStartNotification; extern NSString *const SDWebImageDownloadStopNotification; |
定義了三個block
- 第一個返回已經接收的圖片資料的大小,未接收的圖片資料的大小,
- (void)sd_setImageWithPreviousCachedImageWithURL: placeholderImage: options: progress:completed:
這個方法裡面就有用到,因為圖片的下載是需要時間的,所以這個block回撥不止回撥一次,會一直持續到圖片完全下載或者下載失敗才會停止回撥 - 第二個block回撥 下載完成的圖片 , 圖片的資料 , 如果有error返回error ,以及下載是否完成的BOOl值
- 第三個是header過濾:設定一個過濾器,為下載圖片的HTTP request選取header.最終使用的headers是經過這個block過濾時候的返回值
1 2 3 4 5 |
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize); typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished); typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers); |
定義的屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/** * 解壓已經下載快取起來的圖片可以提高效能,但是會消耗大量的記憶體 * 預設為YES顯示比較高質量的圖片,如果你遇到因記憶體消耗過多而造成崩潰的話可以設定為NO, */ @property (assign, nonatomic) BOOL shouldDecompressImages; //下載佇列最大的併發數,意思是佇列中最多同時執行幾條執行緒(全域性搜尋了一下,預設值是3) @property (assign, nonatomic) NSInteger maxConcurrentDownloads; /** * 當前在下載佇列的操作總數,只讀(這是一個瞬間值,因為只要一個操作下載完成就會移除下載佇列) */ @property (readonly, nonatomic) NSUInteger currentDownloadCount; /** * 下載操作的超時時間,預設是15s */ @property (assign, nonatomic) NSTimeInterval downloadTimeout; /** * 列舉型別,代表著操作下載的順序 */ @property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder; //SDWebImageDownloaderExecutionOrder 的定義 typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { /** * 預設值,所有的下載操作以佇列型別(先進先出)執行 */ SDWebImageDownloaderFIFOExecutionOrder, /** * 所有的下載操作以棧型別(後進後出)執行 */ SDWebImageDownloaderLIFOExecutionOrder }; /** * SDWeImageDownloder是一個單例,這是初始化方法 */ + (SDWebImageDownloader *)sharedDownloader; /** * 為request操作設定預設的URL憑據,具體實施為:在將操作新增到佇列之前,將操作的credential屬性值設定為urlCredential */ @property (strong, nonatomic) NSURLCredential *urlCredential; /** * Set username */ @property (strong, nonatomic) NSString *username; /** * Set password */**區域性常量** @property (strong, nonatomic) NSString *password; /** * 設定一個過濾器,為下載圖片的HTTP request選取header.意味著最終使用的headers是經過這個block過濾之後的返回值。 */ @property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter; |
看完這些屬性後我們在來看SDWebImageDownloader裡面的兩個核心方法,其他的方法會捎帶說一下
第一個就是一開始我們說的,SDWebImageManager會呼叫的方法
1 2 3 4 5 6 7 8 9 10 11 12 |
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak __typeof(self)wself = self; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ //這裡面都是建立下載的回撥 }]; } |
先來看看-addProgressCallback:completedBlock:forURL:createCallback:
裡面都做了些什麼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. //如果圖片的url是空的就直接返回 if (url == nil) { if (completedBlock != nil) { completedBlock(nil, nil, nil, NO); } return; } dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download request for the same URL NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; if (first) { createCallback(); } }); } |
下面重點也是不太好理解的東西,我也是又系統地複習了一下GCD,琢磨了有段時間才繼續寫的
1 2 3 4 5 |
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } |
如果你GCD非常熟悉就跳過吧,不熟悉就先來看看我總結的GCD吧,寫的比較好理解,先來看看 幾個概念
Serial 序列 Concurrent併發
任務序列執行每次只有一個任務執行
任務併發執行就是同一時間可以有多個任務被執行
Synchronous 同步
一個同步函式只有在它完成預定的任務才返回(返回的意思是:返回當前執行緒,執行緒繼續向下執行任務,你可以自己做個測試用一個同步函式,任務裡面sleep(3);測試一下就明白了)
Asynchronous 非同步
一個非同步函式,會立即返回,預定任務會完成,但是不會等到這個任務完成才返回
Queues 佇列
GCD提供 dispatch queues來處理程式碼,這些佇列管理你提供給GCD的任務並用FIFO順序執行,這保證了第一個被新增到佇列裡的任務會是佇列中第一個執行的,第二個被新增的任務第二個開始執行,如此直到佇列的終點
只能保證任務開始的順序不能保證任務結束的順序
Serial Queues 序列佇列
序列佇列的任務一次執行一個,每一個任務只有在前一個任務完成的時候才開始,但是你不知道一個任務(block)和下一個開始之間的時間長度
Concurrent Queues 併發佇列
在併發佇列中的任務能得到的保證是它們會被按照被新增的順序開始執行,任務能以任意順序完成,但是你不知道什麼時候才開始執行下一個任務,或者任意時刻有多少block在執行,這完全取決於GCD
Queue Type 佇列型別
主佇列(main queue),和其它序列佇列一樣,這個佇列中的任務一次只能執行一個,然後它能保證所有的任務都在主執行緒執行,而主執行緒是唯一可用於更新UI的執行緒,這個佇列就是用於發訊息給UIView或傳送通知的
全域性排程佇列(Global Dispatch Queues),它分了四種優先順序(任務執行的優先順序):background , low , default , high
Apple的API也會使用這些佇列,所以你新增的任何任務都不會是這些佇列唯一的任務
自己建立的序列佇列 或者併發佇列
GCD提供的函式
dispatch_async 非同步 , 與其他執行緒無關
dispatch_sync 同步,阻塞其他執行緒
dispatch_apply 重複執行
dispatch_after 延遲執行
dispatch_barrier_async dispatch_barrier_sync(下面細講)
只列舉了一些常用的GCD函式,並不完全
GCD的使用呢,總結起來就是先選用一個GCD提供的函式,傳入一個你要呼叫的佇列(三種佇列型別的一種)和一個block(任務),
佇列會在輪到這個block執行的時候執行這個block
注意:佇列是用來存放任務的,佇列並不等於執行緒,佇列中存放的任務最後都要由執行緒來執行
再回到剛才要看的部分,dispatch_barrier_sync
是我們選用的GCD提供的函式,self.barrierQueue
是存放任務的佇列,block裡面是要執行的任務
1 2 3 4 5 6 |
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } |
先來看看dispatch_barrier_sync
Dispatch Barrier解決多執行緒併發讀寫一個資源發生死鎖
sync說明了這是個同步函式,任務不會立即返回,會等到任務執行結束才返回
使用dispatch_barrier_sync
此函式建立的任務會首先去檢視佇列中有沒有別的任務要執行,如果有則會等待已有任務執行完畢再執行;同時在此方法後新增的任務必須等到此方法中任務執行後才能執行,利用這個方法可以控制執行順序
Dispatch Barrier
確保提交的block是指定佇列中特定時段唯一在執行的一個.在所有先於Dispatch Barrier的任務都完成的情況下這個block才開始執行.輪到這個block時barrier會執行這個block並且確保佇列在此過程 不會執行其他任務.block完成後才恢復佇列
1 |
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue",DISPATCH_QUEUE_CONCURRENT); |
這是使用者自己建立的佇列,DISPATCH_QUEUE_CONCURRENT代表的是它是一個並行佇列,為什麼選擇併發佇列而不是序列佇列我們來想一下:
序列佇列可以保證任務按照新增的順序一個個開始執行,並且上一個任務結束才開始下一個任務,這已經可以保證任務的執行順序(或者說是任務結束的順利)了,但是並行佇列不一樣,併發佇列只能保證任務的開始,至於任務以什麼樣的順序結束並不能保證但是併發佇列使用Barrier
卻是可以保證的
這部分就先到這裡繼續向下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } NSMutableArray *callbacksForURL = self.URLCallbacks[url]; NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; |
URLCallbacks是一個可變字典,key是NSURL型別,value為NSMutableArray型別,value(陣列裡面)只包含一個元素,這個元素的型別是NSMutableDictionary型別,這個字典的key為NSString型別代表著回撥型別,value為block,是對應的回撥
這些程式碼的目的都是為了給url繫結回撥
繼續向下看:
1 2 3 |
if (first) { createCallback(); } |
如果url第一次繫結它的回撥,也就是第一次使用這個url建立下載任務則執行一次建立回撥
在建立回撥中 建立下載操作(下載操作並不是在這裡建立的),dispatch_barrier_sync
執行確保同一時間只有一個執行緒操作URLCallbacks屬性,也就是確保了下面建立過程中在給operation傳遞迴調的時候能取到正確的self.URLCallbacks[url]值,同事確保後面有相同的url再次建立的時候if (!self.URLCallbacks[url])
分支不再進入,first==NO,也就不再繼續呼叫建立回撥,這樣就確保了同一個url對應的圖片不會重複下載
以上這部分程式碼總結起來只做了一件事情:在barrierQueue佇列中建立下載任務
至此下載的任務都建立好了,下面該輪到下載的操作了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{ //建立下載的回撥,我們開始來看看建立完下載的回撥之後裡面都寫了什麼事情 //配置下載超時的時間 NSTimeInterval timeoutInterval = wself.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } /** 建立請求物件,並根據options引數設定其屬性 為了避免潛在的重複快取(NSURLCache + SDImageCache), 如果沒有明確告知需要快取, 則禁用圖片請求的快取操作, 這樣就只有SDImageCache進行了快取 這裡的options 是SDWebImageDownloaderOptions */ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; // 通過設定 NSMutableURLRequest.HTTPShouldHandleCookies = YES //的方式來處理儲存在NSHTTPCookieStore的cookies request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //返回在接到上一個請求得得響應之前,飾釦需要傳輸資料,YES傳輸,NO不傳輸 request.HTTPShouldUsePipelining = YES; }]; }; /** 如果你自定義了wself.headersFilter,那就用你自己設定的 wself.headersFilter來設定HTTP的header field 它的定義是 typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers); 一個返回結果為NSDictionary型別的block 如果你沒有自己設定wself.headersFilter那麼就用SDWebImage提供的HTTPHeaders HTTPHeaders在#import "SDWebImageDownloader.h",init方法裡面初始化,下載webp圖片需要的header不一樣 (WebP格式,[谷歌]開發的一種旨在加快圖片載入速度的圖片格式。圖片壓縮體積大約只有JPEG的2/3,並能節省大量的伺服器頻寬資源和資料空間) #ifdef SD_WEBP _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; #else _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; #endif */ if (wself.headersFilter) { request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = wself.HTTPHeaders; } /** 建立SDWebImageDownLoaderOperation操作物件(下載的操作就是在SDWebImageDownLoaderOperation類裡面進行的) 傳入了進度回撥,完成回撥,取消回撥 @property (assign, nonatomic) Class operationClass; 將Class作為屬性儲存,初始化具體Class,使用的時候呼叫具體class的方法 */ operation = [[wself.operationClass alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { //progress block回撥的操作 SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; /** URLCallbacks是一個字典,key是url,value是一個陣列, 陣列裡面裝的是字典,key是NSString代表著回撥型別,value為block是對應的回撥 確保提交的block是指定佇列中特定時段唯一在執行的一個. */ dispatch_sync(sself.barrierQueue, ^{ //根據key取出裝了字典的陣列 callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ //根據kProgressCallbackKey這個key取出進度的操作 SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //返回已經接收的資料位元組,以及未接收的資料(預計位元組) if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { //completed block 回撥的操作 SDWebImageDownloader *sself = wself; if (!sself) return; //依舊是根據url這個key取出一個裡面裝了字典的陣列 __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { //如果這個任務已經完成,就根據url這個key從URLCallbacks字典裡面刪除 [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { //根據kCompletedCallbackKey這個key取出SDWebImageDownloaderCompletedBlock(完成的block) SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; //回撥 圖片 data error 是否完成的 if (callback) callback(image, data, error, finished); } } cancelled:^{ //將url對應的所有回撥移除 SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; //上面 是SDWebImageDownloaderOperation *operation的建立,從這裡開始就都是對operation的配置 // 設定是否需要解壓 operation.shouldDecompressImages = wself.shouldDecompressImages; /** 使用者認證 NSURLCredential 當連線客戶端與服務端進行資料傳輸的時候,web伺服器 收到客戶端請求時可能需要先驗證客戶端是否是正常使用者,再決定是否返回該介面的真實資料 iOS7.0之前使用的網路框架是NSURLConnection,在 2013 的 WWDC 上, 蘋果推出了 NSURLConnection 的繼任者:NSURLSession SDWebImage使用的是NSURLConnection,這兩種網路框架的認證呼叫的方法也是不一樣的,有興趣的可以去google一下這裡只看下NSURLConnection的認證(在這裡寫看著有些吃力,移步到這個程式碼框外面閱讀) */ if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } //根據下載選項SDWebImageDownloaderHighPriority設定優先順序 if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } //將下載操作加到下載佇列中 [wself.downloadQueue addOperation:operation]; if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { /** 根據executionOrder設定操作的依賴關係 executionOrder代表著下載操作執行的順序,它是一個列舉 SD新增下載任務是同步的,而且都是在self.barrierQueue這個並行佇列中, 同步新增任務。這樣也保證了根據executionOrder設定依賴關是正確的。 換句話說如果建立下載任務不是使用dispatch_barrier_sync完成的,而是使用非同步方法 ,雖然依次新增建立下載操作A、B、C的任務,但實際建立順序可能為A、C、B,這樣當executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設定的操作依賴關係就變成了A依賴C,C依賴B typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { // 預設值,所有的下載操作以佇列型別執行,先被加入下載佇列的操作先執行 SDWebImageDownloaderFIFOExecutionOrder, // 所有的下載操作以棧型別執行,後進先出,後被加入下載佇列的操作先執行 SDWebImageDownloaderLIFOExecutionOrder }; */ [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; } |
NSURLCredential 身份認證
認證過程
1.web伺服器接收到來自客戶端的請求
2.web服務並不直接返回資料,而是要求客戶端提供認證資訊,也就是說挑戰是服務端向客戶端發起的
2.1要求客戶端提供使用者名稱與密碼挑戰 NSInternetPassword
2.2 要求客戶端提供客戶端證書 NSClientCertificate
2.3要求客戶端信任該伺服器
3.客戶端回撥執行,接收到需要提供認證資訊,然後提供認證資訊,並再次傳送給web服務
4.web服務驗證認證資訊
4.1認證成功,將最終的資料結果傳送給客戶端
4.2認證失敗,錯誤此次請求,返回錯誤碼401
Web服務需要驗證客戶端網路請求
NSURLConnectionDelegate 提供的接收挑戰,SDWeImage使用的就是這個方案
1 2 |
-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; |
至此下載管理 SDWebImageDownloader到這裡就算結束了,它的主要作用就是建立下載任務,管理下載任務(取消,下載等狀態改變)這裡的重點就是對self.barrierQueue的理解,最後我們來看看SDWebImageDownloaderOptions下載操作和下載過程的實現
SDWebImageDownloaderOptions
它的作用就是網路請求的配置,進行網路請求以及資料處理
依舊先來看看它公開宣告的屬性和方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
@interface SDWebImageDownloaderOperation : NSOperation /** * 下載時用於網路請求的request */ @property (strong, nonatomic, readonly) NSURLRequest *request; /** * 圖片下載完成是否需要解壓 */ @property (assign, nonatomic) BOOL shouldDecompressImages; /** * :URLConnection是否需要諮詢憑據倉庫來對連線進行授權,預設YES */ @property (nonatomic, assign) BOOL shouldUseCredentialStorage; /** * web服務要求客戶端進行挑戰,用NSURLConnectionDelegate提供的方法接收挑戰,最終會生成一個挑戰憑證,也是NSURLCredential的例項 credential */ @property (nonatomic, strong) NSURLCredential *credential; /** * SDWebImageDownloader.h裡面定義的,一些下載相關的選項 */ @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options; /** * 預期的檔案大小 */ @property (assign, nonatomic) NSInteger expectedSize; /** * connection物件進行網路訪問,接收到的response */ @property (strong, nonatomic) NSURLResponse *response; /** * 用預設的屬性值初始化一個SDWebImageDownloaderOperation物件 */ - (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock; |
然後繼續看SDWebImageDownloaderOperation.h
初始化方法,這個就是初始化一個SDWebImageDownloaderOperation例項,沒什麼看點
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (id)initWithRequest:(NSURLRequest *)request options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock cancelled:(SDWebImageNoParamsBlock)cancelBlock { if ((self = [super init])) { _request = request; _shouldDecompressImages = YES; _shouldUseCredentialStorage = YES; _options = options; _progressBlock = [progressBlock copy]; _completedBlock = [completedBlock copy]; _cancelBlock = [cancelBlock copy]; _executing = NO; _finished = NO; _expectedSize = 0; responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called } return self; } |
但是下面這個方法- (void)start
就是關鍵了,它是對NSOperation- (void)start
的重寫,這個方法是執行下載任務的核心程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
- (void)start { //先加一把執行緒鎖,保證執行到這裡的時候只有當前執行緒在執行下面的方法 @synchronized (self) { //如果下載操作被取消了 if (self.isCancelled) { self.finished = YES; //把下載相關的屬性置為nil [self reset]; return; } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 /** App 進入後臺時,請求繼續執行一段時間的方法, 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系統借用一點時間, 繼續執行下面的程式碼來完成connection的建立和進行下載任務。 */ 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:^{ __strong __typeof (wself) sself = wself; if (sself) { /**在後臺任務執行時間超過最大時間時, 也就是後臺任務過期執行過期回撥。 在回撥主動將這個後臺任務結束。 */ [sself cancel]; [app endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } #endif // 下載任務執行的狀態,在執行是YES,不在執行時NO self.executing = YES; //建立用於下載的connection self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; //獲取當前得得執行緒 self.thread = [NSThread currentThread]; } //開始下載 [self.connection start]; //如果connection建立完成 if (self.connection) { if (self.progressBlock) { //任務開始立刻執行一次進度的回撥 self.progressBlock(0, NSURLResponseUnknownLength); } dispatch_async(dispatch_get_main_queue(), ^{ //傳送開始下載的通知 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); /** 在 [self.connection start];有返回結果(正常完成,有錯誤都算是結果)之前, 程式碼會一直阻塞在CFRunLoopRun()或者CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false) 這裡, 也就是說 [self.connection start];之後下載就一直在進行中,一直到下載完成或者出錯了(這兩種情況都會呼叫CFRunLoopStop),這個阻塞才會解除 */ if (floor(NSFoundationVersionNumber) = __IPHONE_4_0 Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } if (self.backgroundTaskId != UIBackgroundTaskInvalid) { UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif } |
最後,我們來看NSURLConnection (delegate)
1.connection: didReceiveResponse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //如果statusCode0那麼預期檔案的大小就是response.expectedContentLength ,反之就是0 NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0; self.expectedSize = expected; //立即完成一次進度回撥 if (self.progressBlock) { self.progressBlock(0, expected); } //初始化屬性imageDate,用於拼接圖片 二進位制資料 self.imageData = [[NSMutableData alloc] initWithCapacity:expected]; self.response = response; dispatch_async(dispatch_get_main_queue(), ^{ //非同步的 向主隊佇列傳送一個通知 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self]; }); } else { NSUInteger code = [((NSHTTPURLResponse *)response) statusCode]; /** 如果 statusCode == 304 就呼叫[self cancelInternal]方法 ,或者取消self.connection的連線 取消操作,傳送操作停止的通知,執行完成回撥,停止當前的runloop,設定下載完成標記為YES,正在執行的為NO,將屬性置為空 */ if (code == 304) { [self cancelInternal]; } else { [self.connection cancel]; } dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); if (self.completedBlock) { self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES); } CFRunLoopStop(CFRunLoopGetCurrent()); [self done]; } } |
2.connection: didReceiveData
拼接資料的協議
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) { // 根據self.imageData獲取已接收的資料的長度 const NSInteger totalSize = self.imageData.length; /** 每次接收到資料時,都會用現有的資料建立一個CGImageSourceRef物件以做處理, 而且這個資料應該是已接收的全部資料,而不僅僅是新的位元組,所以才使用self.imageData作為引數(注意建立imageSource使用的資料是CoreFoundation的data,但是self.imageData是NSData,所以用(__bridge CFDataRef)self.imageData做轉化 ) */ CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); /** 在首次接收到資料的時候,圖片的長寬都是0(width+height == 0) 先從這些包含影象資訊的資料中取出影象的長,寬,方向等資訊以備使用 */ if (width + height == 0) { //獲取圖片的屬性資訊 CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); if (properties) { NSInteger orientationValue = -1; //圖片畫素的高度 可以前面加(__bridge NSNumber *)轉換為NSNumber型別 CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &height); //獲取圖片的寬度 val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &width); //獲取圖片的朝向 val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); //CoreFoundation物件型別不在ARC範圍內,需要手動釋放資源 CFRelease(properties); /** 使用Core Craphics框架繪製image時,使用的是 initWithCGImage這個函式,但是使用這個函式有時候會造成圖片朝向的錯誤, 所以在這裡儲存朝向資訊,orientation是一個可以記錄圖片方向的列舉 */ orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; } } /** width+height>0 說明這時候已經接收到圖片的資料了 totalSize 0 && totalSize |
3.connectionDidFinishLoading:這個方法完成以後,代理不再會接收人和connection傳送的訊息,標誌著圖片下載完成,一般下載任務正常結束之後就會執行一次這個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { /** 停止當前的runLoop,將connection屬性和thread屬性 傳送下載停止的通知 */ CFRunLoopStop(CFRunLoopGetCurrent()); self.thread = nil; self.connection = nil; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self]; }); } /** 檢查sharedURLCache是否快取了這次下載response 如果沒有就把responseFromCached設定為NO */ if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) { responseFromCached = NO; } /** 執行完成回撥 */ if (completionBlock) { /** 圖片的快取用的都是SDWebCache,所以就算設定了SDWebImageDownloaderIgnoreCachedResponse, responseFromCached 回撥的圖片也是nil(理解有可能有偏差) */ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) { completionBlock(nil, nil, nil, YES); } else if (self.imageData) { //將資料轉換為UIImage型別 UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; image = [self scaledImageForKey:key image:image]; // 注意對於gif圖片,不需要解壓縮 if (!image.images) { if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image]; } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { //如果圖片的大小為0 , 完成回撥報錯 completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { //回撥圖片 已經圖片的大小 完成狀態YES completionBlock(image, self.imageData, nil, YES); } } else { //圖片為空 回撥 報錯 completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES); } } self.completionBlock = nil; //將NSConnection 設定為完成狀態 [self done]; } |
到這裡,看的也差不多了,認真看完感覺這個作者太厲害了,也真的學習到了很多,歡迎交流,也希望大家自己有空了也看一下,這次真的是拖了一個月因為有的東西我沒明白就看了好多天也查了 各種資料,這次也算是盡力寫好了吧,慚愧
包廂裡的狂歡,曲終人散
have Fine
以上