1. 程式人生 > >SDWebImage原始碼解讀《一》

SDWebImage原始碼解讀《一》

前言:

關於SDWebImage的文章網上已經非常多了,今天寫SD相關的一方面算是對優秀的開源框架程式碼學習,另一方面總結一下框架內優秀的思想,知識的積累本身也是在於總結。本篇部落格著重分析一下這幾個類的部分實現:

  • SDWebImageManager
  • SDImageCache
  • SDWebImageDownloader
  • 總結

一、SDWebImageManager

SDWebImageManagerSDWebImage的核心類,管理著SDWebImageDownloaderSDImageCache,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.@autoreleasepoolSD使用自動釋放池對記憶體進行了優化,diskImage物件實際上如果圖片比較大確實會佔用很大記憶體開銷,而且[self diskImageForKey:key]返回的image物件實際為autorelease自動釋放,這樣也導致了此物件只能在下一次事件迴圈中再外層的autoreleasepool中釋放,讓這段時間記憶體增長,影響效能。

三、SDWebImageDownloader

如果本地沒有這張圖片那麼就會進入到imageDownloaderimageDownloader為下載器物件,處理下載圖片的邏輯,那麼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);,也就是說在一個併發佇列上會將queuebarrier前面新增的任務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.對於不同地方下載同一資源的情況,可以嘗試使用SDblock回撥儲存以及回撥時機的策略,保證資源只有一個在下載,而不同調用的地方都能得到回撥,進度回撥或者完成回撥及失敗回撥等等。