在開發專案的過程中會用到很多第三方庫,比如AFNetWorking,SDWebImage,FMDB等,但一直都沒去好好的研究一下,最近剛好專案不是太緊,閒下來可以給自己充充電,先研究一下SDWebImage的底層實現,原始碼地址:SDWebImage
先介紹一下SDWebImage,我們使用較多的是它提供的UIImageView分類,支援從遠端伺服器下載並快取圖片。自從iOS5.0開始,NSURLCache也可以處理磁碟快取,那麼SDWebImage的優勢在哪?首先NSURLCache是快取原始資料(raw data)到磁碟或記憶體,因此每次使用的時候需要將原始資料轉換成具體的物件,如UIImage等,這會導致額外的資料解析以及記憶體佔用等,而SDWebImage則是快取UIImage物件在記憶體,快取在NSCache中,同時直接儲存壓縮過的圖片到磁碟中;還有一個問題是當你第一次在UIImageView中使用image物件的時候,圖片的解碼是在主執行緒中執行的!而SDWebImage會強制將解碼操作放到子執行緒中。下圖是SDWebImage簡單的類圖關係:
下面從UIImageView的圖片載入開始看起,Let’s go!
首先我們在給UIImageView設定圖片的時候會呼叫方法:
1 |
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; |
其中url為遠端圖片的地址,而placeholder為預顯示的圖片。
其實還可以新增一些額外的引數,比如圖片選項SDWebImageOptions
1 2 |
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { SDWebImageRetryFailed = 1 |
一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority,下面看看具體的函式呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { [self sd_cancelCurrentImageLoad];//取消正在下載的操作 objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);//關聯該view對應的圖片URL /*...*/ if (url) { __weak UIImageView *wself = self;//防止retain cricle //由SDWebImageManager負責圖片的獲取 id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { /*獲取圖片到主線層顯示*/ }]; [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } } |
可以看出圖片是從服務端、記憶體或者硬碟獲取是由SDWebImageManager管理的,這個類有幾個重要的屬性:
1 2 3 |
@property (strong, nonatomic, readwrite) SDImageCache imageCache;//負責管理cache,涉及記憶體快取和硬碟儲存 @property (strong, nonatomic, readwrite) SDWebImageDownloader imageDownloader;//負責從網路下載圖片 @property (strong, nonatomic) NSMutableArray *runningOperations;//包含所有當前正在下載的操作物件 |
manager會根據URL先去imageCache中查詢對應的圖片,如果沒有在使用downloader去下載,並在下載完成快取圖片到imageCache,接著看實現:
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 |
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { /*...*/ //根據URL生成對應的key,沒有特殊處理為[url absoluteString]; NSString *key = [self cacheKeyForURL:url]; //去imageCache中尋找圖片 operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { /*...*/ //如果圖片沒有找到,或者採用的SDWebImageRefreshCached選項,則從網路下載 if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { dispatch_main_sync_safe(^{ //如果圖片找到了,但是採用的SDWebImageRefreshCached選項,通知獲取到了圖片,並再次從網路下載,使NSURLCache重新重新整理 completedBlock(image, nil, cacheType, YES, url); }); } /*下載選項設定*/ //使用imageDownloader開啟網路下載 id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { /*...*/ if (downloadedImage && finished) { //下載完成後,先將圖片儲存到imageCache中,然後主執行緒返回 [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; } dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url); } }); } } /*...*/ } else if (image) { //在cache中找到圖片了,直接返回 dispatch_main_sync_safe(^{ if (!weakOperation.isCancelled) { completedBlock(image, nil, cacheType, YES, url); } }); } }]; return operation; } |
下面先看downloader從網路下載的過程,下載是放在NSOperationQueue中進行的,預設maxConcurrentOperationCount為6,timeout時間為15s:
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 |
- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { __block SDWebImageDownloaderOperation *operation; __weak SDWebImageDownloader *wself = self; /*...*/ //防止NSURLCache和SDImageCache重複快取 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy :NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; request.allHTTPHeaderFields = wself.HTTPHeaders;//設定http頭部 //SDWebImageDownloaderOperation派生自NSOperation,負責圖片下載工作 operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:^(NSInteger receivedSize, NSInteger expectedSize) {} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {} cancelled:^{}]; operation.shouldDecompressImages = wself.shouldDecompressImages;//是否需要解碼 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) { // 如果下載順序是後面新增的先執行 [wself.lastAddedOperation addDependency:operation]; wself.lastAddedOperation = operation; } }]; return operation; } |
SDWebImageDownloaderOperation派生自NSOperation,通過NSURLConnection進行圖片的下載,為了確保能夠處理下載的資料,需要在後臺執行runloop:
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 |
- (void)start { /*...*/ #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 //開啟後臺下載 if ([self shouldContinueWhenAppEntersBackground]) { __weak __typeof__ (self) wself = self; self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ __strong __typeof (wself) sself = wself; if (sself) { [sself cancel]; [[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } #endif self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; } [self.connection start]; if (self.connection) { if (self.progressBlock) { self.progressBlock(0, NSURLResponseUnknownLength); } //在主執行緒發通知,這樣也保證在主執行緒收到通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); CFRunLoopRun();//在預設模式下執行當前runlooprun,直到呼叫CFRunLoopStop停止執行 if (!self.isFinished) { [self.connection cancel]; [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]]; } } #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 if (self.backgroundTaskId != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif } |
下載過程中,在代理 - (void)connection:(NSURLConnection )connection didReceiveData:(NSData )data中將接收到的資料儲存到NSMutableData中,[self.imageData appendData:data],下載完成後在該執行緒完成圖片的解碼,並在完成的completionBlock中進行imageCache的快取:
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 |
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection { SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock; @synchronized(self) { CFRunLoopStop(CFRunLoopGetCurrent());//停止當前對runloop /*...*/ if (completionBlock) { /*...*/ UIImage *image = [UIImage sd_imageWithData:self.imageData]; NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; image = [self scaledImageForKey:key image:image]; // Do not force decoding animated GIFs if (!image.images) { if (self.shouldDecompressImages) { image = [UIImage decodedImageWithImage:image];//圖片解碼 } } if (CGSizeEqualToSize(image.size, CGSizeZero)) { completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES); } else { completionBlock(image, self.imageData, nil, YES); } } } self.completionBlock = nil; [self done]; } |
後續的圖片快取可以參考:SDWebImage原始碼剖析(二)