最新源碼更新:
SD外部API沒變,但是內部的封裝和邏輯有一些小小的改變,最近把YYKit也全部分析了一遍,由於之前也看過SD,回頭一看已經變了,再分析一遍最新的
筆記:
validOperationKey --- > [self class]
SDOperationDictionary ---> 通過Category Runtime 掛載到UIView控制項
內部儲存 SDWebImageCombinedOperation ( 這裡有個 cacheOperation 用來儲存緩衝的操作 )
最新調用順序
1.通用方法 - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder
2.SDWebImageManager 單例調用
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock;Manage的兩個單利@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache; // 處理緩衝@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader; // 處理網路下載
2.1 Manage上面那個方法下面初始化
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
2.2 SDImageCache------> 緩衝單例
operation.cacheOperation = [self.imageCache queryCacheOperationForKey。。。] 二級緩衝尋找方法,把隊列返回賦值給Operation的Cache欄位
2.2.1 記憶體緩衝 NSCache 磁碟緩衝 FileManager檔案儲存體 無論尋找與否都返回,只是image是否為空白的區別
2.3 SDWebImageDownloader------->下載單例 (URLOperations 該欄位用來儲存下載的操作)
在上面queryCache的操作回調之後根據尋找的情況調用[[self.imageDownloader downloadImageWithURL......]
2.3.1 調用 addProgressCallback 該方法下的Block是建立 SDWebImageDownloaderOperation 下載隊列用的 2.3.2 SDWebImageDownloaderOperation *operation = self.URLOperations[url]; 根據URL取,沒有的話調用上面的Block建立下載隊列返回 2.3.3 返回 SDWebImageDownloadToken 對象 裡面有url(用來key查詢) 還有 downloadOperationCancelToken 儲存回調Block(complete和Progress) 2.3.4 建立的時候 NSOperationQueue 裡面添加建立的任務,然後就會調用Start(需要重寫) 進行下載,代理回調,接收資料 2.3.5 NSURLSession didReceiveData 返回image
2.4 返回到SDWebImageManager的Block 二級緩衝 記憶體和磁碟緩衝起來 (這裡的緩衝是預設7天,應用退到後台會進行緩衝清理)
3. callCompletionBlockForOperation 調用最終的Block回調到使用者UI控制項進行賦值
伏筆:每次調用外層API的時候都會調用cancel的方法,原理部落格上面有介紹了,這裡說一下具體是怎麼實現的。
首先:UI層面的API下面會調用 sd_internalSetImageWithURL 核心方法進行下載
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
這裡會把返回的 SDWebImageCombinedOperation 和UI控制項進行Category掛載
其次:SDWebImageMangae 調用loadImageWithURL 初始化 SDWebImageCombinedOperation
該對象的的CacheOperation是 SDImageCache 調用直接返回NSOperation賦值 (取消快取作業)
代碼底部還對loadOperation添加了一個Block operation.cancelBlock = ^{取消網路下載的操作};
最後:sd_cancelImageLoadOperationWithKey 調用取消
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation> 已經實現cancel代理
最終調用 SDWebImageCombinedOperation 裡面的cache欄位取消快取作業,調用CancelBlock取消網路下載
幾個核心邏輯沒變,只是實現方式改變了,之前看過一遍,現在看一遍也很快,熟悉的朋友可以自己在看一遍
更新:SDWebImageDecoder是用來圖片的解壓縮(原理以及為什麼)
SD內部已經幫我們把請求回來的資料或者緩衝到本地的圖片資源都進行了非同步解壓縮,因此不需要我們來做,簡單瞭解下
圖片壓縮流程
假設我們使用 +imageWithContentsOfFile: 方法從磁碟中載入一張圖片,這個時候的圖片並沒有解壓縮; 然後將產生的 UIImage 賦值給 UIImageView ; 接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化; 在主線程的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操作,而受圖片是否位元組對齊等因素的影響,這個 copy 操作可能會涉及以下部分或全部步驟: 分配記憶體緩衝區用於管理檔案 IO 和解壓縮操作; 將檔案資料從磁碟讀到記憶體中; 將壓縮的圖片資料解碼成未壓縮的位元影像形式,這是一個非常耗時的 CPU 操作; 最後 Core Animation 使用未壓縮的位元影像資料渲染 UIImageView 的圖層。
在上面的步驟中,我們提到了圖片的解壓縮是一個非常耗時的 CPU 操作,並且它預設是在主線程中執行的。那麼當需要載入的圖片比較多時,就會對我們應用的響應性造成嚴重的影響,尤其是在快速滑動的列表上,這個問題會表現得更加突出。
點擊開啟連結摘自大神的部落格,需要詳細的可以點進去看看
個人理解:
1.首先PNG和JPEG都是圖片的壓縮格式,PNG是無損壓縮,支援alpha,JPEG有損,可選1-100壓縮比例
2.例如你有一張PNG的圖片 20B,那麼你對應的圖片位元據也是20B的,解壓縮後的圖片就可能是幾百幾千B了
3.具體計算公式就是像素寬*像素高*每個像素對應的位元組
4.那麼當你進行圖片渲染的時候,必須得到解壓縮後的原始像素資料,才能進行圖形渲染,這就是解壓縮的原因
SD在SDWebImageDecoder這個檔案中進行了強制解壓縮,我們賦值給imageView的時候已經是解壓縮的檔案了,因此不會卡主主線程,不然預設是在主線程進行解壓縮,圖片一多,卡爆了
平時項目中用的庫挺多的,沒有認認真真看過幾個,給自己定了個計劃今年把SDWebImage,AFNetWorking看完,有時間把YYKit的組件也看看,雖然才這麼幾個,我感覺看完這些能學習個百分之一的代碼邏輯,我就知足了,最近花了一周的時間在網上看了SDWebImage的源碼分析以及SD本身代碼的邏輯分析,分別都加上了註解,畢竟這是邁出去的第一步,看完一遍之後呢,偶然在網上看到一個圖,加上自己的理解,覺得這個圖太合適了,詮釋的很完整
上圖 圖片出處
這個圖分四個部分,聽完我的分析你就能基本上理解了
首先把SD的所有類的作用簡單介紹下
3.8.1版本所有類的作用介紹一下
NSData+ImageContentType 通過Image data判斷當前圖片的格式
SDImageCache 緩衝 定義了 Disk 和 memory二級緩衝(NSCache)負責管理cache 單例
SDWebImageCompat 保證不同平台/版本/螢幕等相容性的宏定義和內聯 圖片縮放
SDWebImageDecoder 圖片解壓縮,內部只有一個介面
SDWebImageDownloader 非同步圖片下載管理,管理下載隊列,管理operation 管理網路請求 處理結果和異常 單例
存放網路請求回調的block 自己理解的資料結構大概是
// 結構{"url":[{"progress":"progressBlock"},{"complete":"completeBlock"}]}
SDWebImageDownloaderOperation 實現了非同步下載圖片的NSOperation,網路請求給予NSURLSession 代理下載
自訂的Operation任務對象,需要手動實現start cancel等方法
SDWebImageManager 核心管理類 主要對緩衝管理 + 下載管理進行了封裝 主要介面downloadImageWithURL單利
SDWebImageOperation operation協議 只定義了cancel operation這一介面 上面的downloaderOperation的代理
SDWebImagePrefetcher 低優先順序情況下預先下載圖片,對SDWebImageViewManager進行簡單封裝 很少用
MKAnnotationView+WebCache – 為MKAnnotationView非同步載入圖片
UIButton+WebCache 為UIButton非同步載入圖片
UIImage+GIF 將Image data轉換成指定格式圖片
UIImage+MultiFormat 將image data轉換成指定格式圖片
UIImageView+HighlightedWebCache 為UIImageView非同步載入圖片
UIImageView+WebCache 為UIImageView非同步載入圖片
UIView+WebCacheOperation 儲存當前MKAnnotationView / UIButton / UIImageView非同步下載圖片的operations
看完基本就這樣的表情,沒關係,繼續往下看,最後會給個完整的總結
看下核心類的基本操作圖
自己花的簡圖,湊活看吧
OK,我們就開始根據調用的代碼一步步按照邏輯走一遍
1.第一步(外部控制項)
直接調用sd暴露在外面的方法
__weak typeof(self)weakSelf = self; [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201111/08/20111108113800_wYcvP.thumb.600_0.jpg"] placeholderImage:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { if (image && cacheType == SDImageCacheTypeNone) { weakSelf.imageView.alpha = 0; [UIView animateWithDuration:1.0f animations:^{ weakSelf.imageView.alpha = 1.f; }]; } else { weakSelf.imageView.alpha = 1.0f; } }]; 這個很簡單,大家也經常用,用的時候打一下sd機會出來一串方法,這一串方法最終在內部都會轉換成
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
2.第二步(UIImageView + WebCache)
// 以上所有的調用方法,最終都是進入這個方法進行圖片的載入// url 載入的圖片// placeholder 佔位圖// options 下載圖片的各種花式設定 一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { // 取消當前的下載操作 如果不取消,那麼當tableView滑動的時候,當前cell的imageView會一直去下載圖片,然後優先顯示下載完成的圖片,直接錯亂 [self sd_cancelCurrentImageLoad]; {......省略一段代碼} // 判斷是否存在 if (url) { {......省略一段代碼} __weak __typeof(self)wself = self; // 關鍵類 SDWebImageManager 來處理圖片下載 // 下載有三層 1當前manager調用下載 2從緩衝中擷取,hit失敗用SDWebImageDownloader對象調用下載 3.SDWebImageDownloaderOperation最終用繼承 // NSOperation的對象NSURLSession的方法去下載圖片,代理裡面進行操作 id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; // 設定了SDWebImageAvoidAutoSetImage 預設不會將UIImage添加進UIImageView對象裡面,而放置在conpleteBlock裡面交由調用方自己處理,做個濾鏡或者淡入淡出什麼的 if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } // 這裡是最終回調出去的block if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; // 儲存本次operation,如果發生多次圖片請求可以用來取消 // 先取消當前UIImageView正在下載的任務,然後在儲存operations // 也就是說當動態綁定的字典裡面的key value對應一個圖片下載 單個圖片value數組就是0,不然就是多個,下載完就會根據key移除 [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); }} 分析下:
這裡首先會調用cancel的方法,為什麼會這樣呢。。解釋下會發生的bug
正常情況下螢幕展示的cell是這樣的,當網路不穩定的情況下,第一個cell還在請求資料的時候,使用者直接下滑了
就會出現下面這樣的狀態,這個是tableView的複用簡單圖解,當列表後面的cell準備載入的時候,會從複用池中找,有的話直接拿出來用,那麼複用池子裡面的cell還是第一個cell指向的imageView指標,如果沒有停止之前的網路請求,那麼直接拿出來再根據後面的資料繫結進行請求就會發生資料錯位,這是非常可怕的。原因就是如果沒有取消之前的請求,imageView的原理就是優先展示最新下載完之後的圖片,就會立馬顯示出來,所以一定要先取消之前的
再來個例子:
一個imageView請求了兩張圖片,1.png 和 2.png,但我們只希望顯示 2.png,所以需要取消 1.png的請求。原因有兩點:
1.在非同步請求中(先後順序不定),有可能 1.png 會在 2.png 後面擷取到,會覆蓋掉2.png
2.減少網路請求,網路請求是一個很耗時的操作
然後這一步的重點就是會啟用SDWebImageManager管理單例,調用他的方法進行網路請求
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
3.第三步(SDWebImageManager)
這個管理類有兩個得力的手下
一個是SDImageCache 專門管理緩衝
A:NSCache負責記憶體緩衝,用法和NSDictionary基本一樣
B:磁碟緩衝用NSFileManager寫檔案的方式完成
註:
1. NSCache具有自動刪除的功能,以減少系統佔用的記憶體,還能設定記憶體臨界值
2. NSCache是安全執行緒的,不需要加線程鎖;
3. 鍵對象不會像 NSMutableDictionary 中那樣被複製。(鍵不需要實現 NSCopying 協議)。
// 從imageCache中尋找圖片//每次向SDWebImageCache索取圖片的時候,會先根據圖片URL對應的key值先檢查記憶體中是否有對應的圖片,如果有則直接返回;如果沒有則在ioQueue中去硬碟中尋找,其中檔案名稱是是根據URL產生的MD5值,找到之後先將圖片緩衝在記憶體中,然後在把圖片返回:- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock { if (!doneBlock) { return nil; } if (!key) { doneBlock(nil, SDImageCacheTypeNone); return nil; } // First check the in-memory cache... // 1.先去記憶體層面尋找 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { doneBlock(image, SDImageCacheTypeMemory); return nil; } // 如果在記憶體沒找到 // 2. 如果記憶體中沒有,則在磁碟中尋找。如果找到,則將其放到記憶體緩衝,並調用doneBlock回調 NSOperation *operation = [NSOperation new]; dispatch_async(self.ioQueue, ^{ if (operation.isCancelled) { return; } // 建立自動釋放池,記憶體即時釋放// 如果你的應用程式或者線程是要長期啟動並執行並且有可能產生大量autoreleased對象, 你應該使用autorelease pool blocks @autoreleasepool { // 從硬碟拿,拿到了根據欄位存入記憶體 UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.shouldCacheImagesInMemory) { // 像素 NSUInteger cost = SDCacheCostForImage(diskImage); // 緩衝到NSCache中 [self.memCache setObject:diskImage forKey:key cost:cost]; } dispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, SDImageCacheTypeDisk); }); } }); return operation;}
小知識點:
這裡開了非同步串列隊列去Disk中尋找,保證不阻塞主線程,而且開了autoreleasepool以降低記憶體暴漲問題,能得到及時釋放,如果能取到,首先緩衝到記憶體中然後再回調
如果記憶體和磁碟中都取不到圖片,就會讓Manager的另一個手下SDWebImageDownloader去下載圖片
A:這貨也是一個單例,專門負責圖片的下載圖片的下載都是放在NSOperationQueue中完成的
// 下載圖片的最終方法實現- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { {......} // 根據URL產生對應的key 沒有特殊處理為[self absoluteString] NSString *key = [self cacheKeyForURL:url]; // 前面的操作。主要作了如下處理: // 1. 判斷url的合法性 // 2. 建立SDWebImageCombinedOperation對象 // 3. 查看url是否是之前下載失敗過的 // 去imageCache中尋找圖片 operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { if (operation.isCancelled) { @synchronized (self.runningOperations) { [self.runningOperations removeObject:operation]; } return; } // 如果圖片沒有找到 或者是SDWebImageRefreshCached 就從網路上下載圖片 if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { if (image && options & SDWebImageRefreshCached) { dispatch_main_sync_safe(^{ // 如果圖片存在cache中,但是options還是SDWebImageRefreshCached 通知cache去重新重新整理緩衝圖片 completedBlock(image, nil, cacheType, YES, url); }); } // 設定下載選項屬性 {......} // 開啟下載 // 這裡的兩個回調都是從DownloaderOperation裡面出來的,progressBlock是不要取到的,直接在最外層調用的地方處理,完成的話需要進行cache,因此要在這裡處理回調,處理完再回調出去 id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { // 上面的是weak的這裡設定成strong 避免被釋放掉了 __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) { // 如果出錯,則調用完成回調,並將url放入下載挫敗url數組中 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) { @synchronized (self.failedURLs) { [self.failedURLs addObject:url]; } } } else { // 當重新下載的時候能擷取到了,那麼久把他從之前的failURL裡面移除 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:)]) { // 在全域隊列中平行處理圖片的緩衝 // 首先對圖片做個轉換操作,該操作是代理對象實現的 // 然後對圖片做緩衝處理 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]; if (transformedImage && finished) {