投稿文章,作者:missummer(簡書)
上一篇我們基本上看完了SDWebImage整個工作流程,下面我們具體看一下緩存下載圖片中涉及到的相關的類。
SDWebImageDownloader
SDWebImageManager實現下載依賴於下載器:SDWebImageDownloader,下載器負責管理下載任務,而執行下載任務是由SDWebImageDownloaderOperation操作完成。
SDWebImageManager實現下載 就是調用下面這個方法:
- (id)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock
我們還是先來看看SDWebImageDownloader裡面都寫了些什麼:
SDWebImageDownloader.h
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { //這個屬於默認的使用模式了,前往下載,返回進度block信息,完成時調用completedBlock SDWebImageDownloaderLowPriority = 1 << 0, //漸進式下載 ,如果設置了這個選項,會在下載過程中,每次接收到一段返回數據就會調用一次完成回調,回調中的image參數為未下載完成的部分圖像,可以實現將圖片一點點顯示出來的功能 SDWebImageDownloaderProgressiveDownload = 1 << 1, /** * 通常情況下request阻止使用NSURLCache.這個選項會默認使用NSURLCache */ SDWebImageDownloaderUseNSURLCache = 1 << 2, /** * 如果從NSURLCache中讀取圖片,會在調用完成block的時候,傳遞空的image或者imageData */ SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, /** * 系統為iOS 4+時候,如果應用進入後台,繼續下載.這個選項是為了實現在後台申請額外的時間來完成請求.如果後台任務到期,操作也會被取消 */ SDWebImageDownloaderContinueInBackground = 1 << 4, /** * 通過設置 NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來處理存儲在NSHTTPCookieStore的cookies */ SDWebImageDownloaderHandleCookies = 1 << 5, /** * 允許不受信任的SSL證書,在測試環境中很有用,在生產環境中要謹慎使用 */ SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, /** * 將圖片下載放到高優先級隊列中 */ SDWebImageDownloaderHighPriority = 1 << 7, };
這些選項主要涉及到下載的優先級,緩存,後台任務執行,cookie處理以及證書認證幾個方面,在創建下載操作的時候可以使用組合的選項來完成一些特殊的需求
定義裡兩個常量,後面通知的時候用的,這裡的常量是全局常量
全局常量:不管你定義在任何文件夾,外部都能訪問
const NSString *myName = @"楊千嬅染了紅頭發";
局部常量:用static修飾後,不能提供外界訪問(只能在賦值的.m文件使用,外界不可訪問)
static const NSString *myName= @"楊千嬅染了紅頭發"; //官方也更推薦這樣定義常量 而不是用#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過濾時候的返回值
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);
定義的屬性
/** * 解壓已經下載緩存起來的圖片可以提高性能,但是會消耗大量的內存 * 默認為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會調用的方法
- (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:裡面都做了些什麼
- (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,琢磨了有段時間才繼續寫的
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裡面是要執行的任務
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完成後才恢復隊列。
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue",DISPATCH_QUEUE_CONCURRENT);
這是用戶自己創建的隊列,DISPATCH_QUEUE_CONCURRENT代表的是它是一個並行隊列,為什麼選擇並發隊列而不是串行隊列我們來想一下:
串行隊列可以保證任務按照添加的順序一個個開始執行,並且上一個任務結束才開始下一個任務,這已經可以保證任務的執行順序(或者說是任務結束的順利)了,但是並行隊列不一樣,並發隊列只能保證任務的開始,至於任務以什麼樣的順序結束並不能保證但是並發隊列使用Barrier卻是可以保證的
這部分就先到這裡繼續向下看:
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綁定回調
繼續向下看:
if (first) { createCallback(); }
如果url第一次綁定它的回調,也就是第一次使用這個url創建下載任務則執行一次創建回調
在創建回調中 創建下載操作(下載操作並不是在這裡創建的),dispatch_barrier_sync執行確保同一時間只有一個線程操作URLCallbacks屬性,也就是確保了下面創建過程中在給operation傳遞回調的時候能取到正確的self.URLCallbacks[url]值,同事確保後面有相同的url再次創建的時候if (!self.URLCallbacks[url])分支不再進入,first==NO,也就不再繼續調用創建回調,這樣就確保了同一個url對應的圖片不會重復下載
以上這部分代碼總結起來只做了一件事情:在barrierQueue隊列中創建下載任務
至此下載的任務都創建好了,下面該輪到下載的操作了:
- (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使用的就是這個方案
-(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
至此下載管理 SDWebImageDownloader到這裡就算結束了,它的主要作用就是創建下載任務,管理下載任務(取消,下載等狀態改變)這裡的重點就是對self.barrierQueue的理解,最後我們來看看SDWebImageDownloaderOptions下載操作和下載過程的實現
SDWebImageDownloaderOptions
它的作用就是網絡請求的配置,進行網絡請求以及數據處理
依舊先來看看它公開聲明的屬性和方法
@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實例,沒什麼看點
- (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的重寫,這個方法是執行下載任務的核心代碼
- (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:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { //如果statusCode<400並且不等304 if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) { //設置文件的預期大小,如果response.expectedContentLength >0那麼預期文件的大小就是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拼接數據的協議
- (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 < self.expectedSize 說明圖片 還沒有接收完全 */ if (width + height > 0 && totalSize < self.expectedSize) { // 創建圖片 CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); #ifdef TARGET_OS_IPHONE // Workaround for iOS anamorphic(失真的 , 變形的) image if (partialImageRef) { const size_t partialHeight = CGImageGetHeight(partialImageRef); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (bmContext) { CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); CGImageRelease(partialImageRef); partialImageRef = CGBitmapContextCreateImage(bmContext); CGContextRelease(bmContext); } else { CGImageRelease(partialImageRef); partialImageRef = nil; } } #endif if (partialImageRef) { UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; UIImage *scaledImage = [self scaledImageForKey:key image:image]; if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:scaledImage]; } else { image = scaledImage; } CGImageRelease(partialImageRef); dispatch_main_sync_safe(^{ if (self.completedBlock) { self.completedBlock(image, nil, nil, NO); } }); } } CFRelease(imageSource); } if (self.progressBlock) { self.progressBlock(self.imageData.length, self.expectedSize); } }
3.connectionDidFinishLoading:這個方法完成以後,代理不再會接收人和connection發送的消息,標志著圖片下載完成,一般下載任務正常結束之後就會執行一次這個方法
- (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
以上