SDWebImage原始碼解讀《一》
關於SDWebImage的文章網上已經非常多了,今天寫SD相關的一方面算是對優秀的開源框架程式碼學習,另一方面總結一下框架內優秀的思想,知識的積累本身也是在於總結。本篇部落格著重分析一下這幾個類的部分實現:
SDWebImageManager SDImageCache SDWebImageDownloader 總結
一、SDWebImageManager
SDWebImageManager
是SDWebImage
的核心類,管理著SDWebImageDownloader
和SDImageCache
,SDWebImageDownloader
為圖片下載器物件,裡面主要管理著SDWebImageDownloaderOperation
進行對圖片的下載,SDImageCache
主要是處理圖片快取相關,先分析一下SDWebImageManage
看看它都做了什麼:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { //封裝下載操作物件 __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; BOOL isFailedUrl = NO; //防止多執行緒訪問出錯,加互斥鎖對self.failedURLs進行保護 @synchronized (self.failedURLs) { isFailedUrl = [self.failedURLs containsObject:url]; } if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { dispatch_main_sync_safe(^{ NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]; completedBlock(nil, error, SDImageCacheTypeNone, YES, url); }); return operation; } //新增互斥鎖 @synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; //根據key查詢快取物件 operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { //是不是被取消了 if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } ... //省略了快取策略相關的程式碼,到這裡是真正的呼叫了imageDownloader下載圖片,imageDownloader的內部實現一會說。 id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (!strongOperation || strongOperation.isCancelled) { // Do nothing if the operation was cancelled // See #699 for more details // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data } else if (error) { dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(nil, error, SDImageCacheTypeNone, finished, url); } }); if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost) { //下載失敗則新增圖片url到failedURLs集合 @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } } } else { //雖然下載失敗,但是如果設定了可以重新下載失敗的url則remove該url if ((options & SDWebImageRetryFailed)) { @synchronized (self.failedURLs) { [self.failedURLs removeObject:url]; } } //是否需要快取在磁碟 BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly); if (options & SDWebImageRefreshCached && image && !downloadedImage) { // Image refresh hit the NSURLCache cache, do not call the completion block } //圖片下載成功並且判斷是否需要轉換圖片 else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) { ... } else { //下載完成且有image則快取圖片 if (downloadedImage && finished) { [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (strongOperation && !strongOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } }); } } if (finished) { @synchronized (self.runningOperations) { if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } } }]; operation.cancelBlock = ^{ [subOperation cancel]; @synchronized (self.runningOperations) { __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation) { [self.runningOperations removeObject:strongOperation]; } } }; } else if (image) { // 有圖片且執行緒沒有被取消,則返回有圖片的completedBlock dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !strongOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } else { //沒有在快取中並且代理方法也不允許下載則回撥失敗 dispatch_main_sync_safe(^{ __strong __typeof(weakOperation) strongOperation = weakOperation; if (strongOperation && !weakOperation.isCancelled) { completedBlock(nil, nil, SDImageCacheTypeNone, YES, url); } }); @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } } }]; return operation; } 複製程式碼
二、SDImageCache
上面是對SDWebImageManager
原始碼做了簡要分析,我想以這裡為入口著重分析一下:
首先SD先呼叫queryDiskCacheForKey:done:
去記憶體中檢視是否有我們要的圖片,那麼這裡面做了什麼呢:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { ... //記憶體中查詢,SD記憶體快取使用NSCache實現。 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } NSOperation *operation = [NSOperation new]; //開闢子執行緒,將block中的任務放入到ioQueue中執行,目的是為了防止io操作阻塞主執行緒,可以看到ioQueue實際上_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);,SD使用GCD非同步序列來實現io操作, 既保證了UI不被阻塞,有能保證block中的程式碼序列執行,防止多執行緒訪問造成資料出錯。 //執行磁碟io操作 dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; } @autoreleasepool { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation; } 複製程式碼
1.self.ioQueue
實際上為_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
,SD
使用GCD
非同步序列來實現io
操作,既保證了UI
不被阻塞,又能保證block
中的程式碼序列執行,實際上包括後面的磁碟寫入操作都是放在這個ioQueue
中執行的,主要目的就是防止多執行緒訪問造成資料競爭導致資料出錯。
2.@autoreleasepool
,SD
使用自動釋放池對記憶體進行了優化,diskImage
物件實際上如果圖片比較大確實會佔用很大記憶體開銷,而且[self diskImageForKey:key]
返回的image
物件實際為autorelease
自動釋放,這樣也導致了此物件只能在下一次事件迴圈中再外層的autoreleasepool
中釋放,讓這段時間記憶體增長,影響效能。
三、SDWebImageDownloader
如果本地沒有這張圖片那麼就會進入到imageDownloader
,imageDownloader
為下載器物件,處理下載圖片的邏輯,那麼imageDownloader
中實現了什麼我們還是看程式碼:
- (id <SDWebImageOperation>)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:^{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; ... //在這裡建立operation物件 operation = [[wself.operationClass alloc] initWithRequest:request inSession:self.session options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; }); for (NSDictionary *callbacks in callbacksForURL) { dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); } } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; dispatch_barrier_sync(sself.barrierQueue, ^{ callbacksForURL = [sself.URLCallbacks[url] copy]; if (finished) { [sself.URLCallbacks removeObjectForKey:url]; } }); for (NSDictionary *callbacks in callbacksForURL) { SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; if (callback) callback(image, data, error, finished); } } cancelled:^{ SDWebImageDownloader *sself = wself; if (!sself) return; dispatch_barrier_async(sself.barrierQueue, ^{ [sself.URLCallbacks removeObjectForKey:url]; }); }]; operation.shouldDecompressImages = wself.shouldDecompressImages; //這一塊在做身份認證,具體下篇說 if (wself.urlCredential) { operation.credential = wself.urlCredential; } else if (wself.username && wself.password) { operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]; } //下載優先順序 if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } [wself.downloadQueue addOperation:operation]; //設定下載的順序 是按照佇列還是棧 if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically adding new operations as last operation's dependency [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; } 複製程式碼
這裡面我著重講一下addProgressCallback:completedBlock:forURL:createCallback:
的實現,看看它裡面都做了什麼事情:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback { ... //省略了一部分原始碼不影響閱讀 //柵欄塊加GCD鎖 dispatch_barrier_sync(self.barrierQueue, ^{ BOOL first = NO; //URLCallbacks 實際上儲存的是所有圖片下載的回撥的可變字典 url為我們請求圖片的地址,以url為key,value為可變陣列。那麼可變陣列中儲存的是什麼呢,往下看。 if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } //取出當前url對應的可變陣列 NSMutableArray *callbacksForURL = self.URLCallbacks[url]; //建立可變字典callbacks,callbacks實際上儲存的是本次下載的進度和完成回撥block。 NSMutableDictionary *callbacks = [NSMutableDictionary new]; if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy]; if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy]; //將此可變字典新增至剛才我們建立的可變陣列callbacksForURL中。 [callbacksForURL addObject:callbacks]; self.URLCallbacks[url] = callbacksForURL; //如果是第一次下載,也就是URLCallbacks儲存所有下載回撥的字典中沒有當前的,那麼認為是第一次下載,執行createCallback()去下載圖片,否則什麼也不做。 if (first) { createCallback(); } }); } 複製程式碼
1.dispatch_barrier_sync
柵欄塊,顧名思義,是做了個攔截,它會將佇列中在它之前的任務執行完畢才會執行它後面的任務,可以理解為一個分界線,而barrierQueue
實際上為_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
,也就是說在一個併發佇列上會將queue
中barrier
前面新增的任務block
全部執行後,再執行barrier
任務的block
,再執行barrier
後面新增的任務block
,這樣一來相當於對block
中的內容加了層鎖,保證執行緒安全。
2.URLCallbacks
是個可變字典,儲存著所有呼叫了下載的block
回撥,如果是第一次下載那麼就執行下載,如果不是第一次那麼就將其回撥儲存在URLCallbacks
裡面,什麼也不做。我以下載進度為例,探究一下URLCallbacks
要幹什麼。在它的回撥裡面是這樣實現的:
//弱引用 SDWebImageDownloader *sself = wself; if (!sself) return; __block NSArray *callbacksForURL; //這裡是我們上面說的柵欄塊block,做執行緒保護 dispatch_sync(sself.barrierQueue, ^{ //此url所對應的所有下載回撥value,陣列型別,儲存的是ui部分對此url所有下載的回撥。 callbacksForURL = [sself.URLCallbacks[url] copy]; }); //遍歷這些回撥 for (NSDictionary *callbacks in callbacksForURL) { //回到主執行緒,為每一個回撥block返回當前圖片的下載進度 dispatch_async(dispatch_get_main_queue(), ^{ SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; if (callback) callback(receivedSize, expectedSize); }); 複製程式碼
四、總結:
1.GCD
的使用,多執行緒加鎖防止資源競爭以及barrier
柵欄塊的使用。
2.如果for
迴圈中或者獲取的資源記憶體開銷較大可以嘗試使用@autoreleasepool
進行記憶體優化。
3.對於不同地方下載同一資源的情況,可以嘗試使用SD
的block
回撥儲存以及回撥時機的策略,保證資源只有一個在下載,而不同調用的地方都能得到回撥,進度回撥或者完成回撥及失敗回撥等等。