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
回撥儲存以及回撥時機的策略,保證資源只有一個在下載,而不同調用的地方都能得到回撥,進度回撥或者完成回撥及失敗回撥等等。