SDWebImage原始碼中閱讀總結-那些不解和收穫.md
SDWebImage原始碼中閱讀總結|那些不解和收穫
圖片怎麼加載出來的?
表中的程式碼位置因我在裡邊寫註釋的原因有些許偏差
流程編號 | 關鍵程式碼 | 程式碼位置 | 描述 | 附加補充 |
---|---|---|---|---|
code_1 | sd_setImageWithURL:placeholderImage: | UIImageView+WebCache.h_line:64 | 入口程式碼,不多解釋 | N |
code_2 | sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options operationKey:(nullable NSString *)operationKey setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock context:(nullable NSDictionary<NSString *, id> *)context | UIView+WebCache.m_line:55 | 所有形式的入口程式碼都彙總到這個方法,隱藏的入口函式 | N |
code_3 | NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); | UIView+WebCache.m_line:65 | 獲取任務標記,一般operationKey都為空,所以,會被預設置為當前類名的字串 | 此處用到了Runtime的關聯 |
code_4 | [self sd_cancelImageLoadOperationWithKey:validOperationKey] | UIView+WebCache.m_line:70 | 如果當前標記下有正在執行的任務,取消執行 | 這個方法的實現有很多值得我們學習的地方 |
code_4.1 | SDOperationsDictionary *operationDictionary = [self sd_operationDictionary] | UIView+WebCacheOperation.m_line:49 | 獲取當前view下關聯的任務hash table,其內部實現是通過“loadOperationKey”作為key去獲取關聯物件,如果獲取不到,則建立一個“NSMapTable”型別的任務hash table,這整個過程在@synchronized(self)保護下,執行緒安全 | 此處用到了@synchronized()確保執行緒安全,使用NSMapTable類建立hash table(比NSDictionary好在哪裡?) |
code_4.2 | objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); | UIView+WebCacheOperation.m_line:72 | 將這個image url 關聯到view 物件上 | 再次用到關聯 |
code_5 | [SDWebImageManager sharedManager]; | UIView+WebCacheOperation.m_line:96 | 獲取SDWebImageManager單例,這是下載、查詢快取的核類 | 此處用到單例確保任務管理的類的唯一性 |
code_6 | loadImageWithURL:options: progress:completed: | UIView+WebCacheOperation.m_line:17,SDWebImageManager.m_line:117 | 開始載入圖片的入口函式,會有一個completed的回撥 | 採用block形式的回撥,程式碼清晰易懂 |
code_6.1 | NSAssert(completedBlock != nil, @“If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead”); | SDWebImageManager.m_line:125 | 如果呼叫載入函式而沒有實現回撥block,會被認為是要預載入圖片,丟擲異常提示使用另外的方法完成預載入 | 此處使用了NSAssert進行友好的提示 |
code_6.2 | SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; | SDWebImageManager.m_line:139 | 初始化一個綜合操作任務 | 將載入任務例項化,因為一個view,一個imageManager會產生多個任務,這樣寫易於對任務的管理和閱讀 |
code_6.3 | isFailedUrl = [self.failedURLs containsObject:url | SDWebImageManager.m_line:148 | 檢視已經失敗的記錄中是否有這個即將處理的url,再次之後如果options包含SDWebImageRetryFailed會直接呼叫完成的回撥 | failedURLs也是一個NSMutableSet型別的集合 |
code_6.4 | [self.runningOperations addObject:operation] | SDWebImageManager.m_line:160 | 將當前的操作任務加入到自身持有的正在執行的記錄中,在此句程式碼前後有兩個鎖,LOCK(self.runningOperationsLock),UNLOCK(self.runningOperationsLock),這兩個巨集使用GCD的訊號量實現加鎖。 | dispatch_semaphore_wait,dispatch_semaphore_signal配合,實現加鎖 |
code_6.5 | NSString *key = [self cacheKeyForURL:url] | SDWebImageManager.m_line:164 | 通過url獲取對應的快取key,裡邊有個可自定義的過濾方法,如果實現了就會呼叫,否則就返回url的absoluteString | 很多部落格寫的都是用url的md5值作為快取的key,在這顯然是不對的,需要把記憶體和磁碟兩種快取分開說,磁碟快取是有MD5操作的 |
code_6.6 | operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:] | UIView+WebCacheOperation.m_line:176 | 查詢快取,這是一個單獨的operation,並且會被當前載入圖片的operation引用 | 這裡相當於在一個一部操作中又產生一個非同步操作,會有執行緒同步的問題存在,比如當前載入圖片的operation被取消了,但是查詢快取的operation依舊在執行,就會產生問題,處理方法我們往後看 |
code_6.6.1 | queryCacheOperationForKey: options: done: | SDImageCache.m_line:514 | 內部流程:1-key判空。2-查詢記憶體快取,如果有快取並且只查詢記憶體快取就呼叫done block回撥。3-查詢磁碟快取,如果上一步有記憶體快取就回調。如果沒有記憶體快取,但是又磁碟快取,這個時候就會把磁碟的圖片解壓,然後放到記憶體快取中(預設,如果不想,通過SDImageCacheConfig中的shouldCacheImagesInMemory屬性控制),然後回撥. | 幾個tip:1,記憶體快取SDMemoryCache,是NSCache的子類,這麼用的優勢是什麼?2,快取重新整理機制。 |
code_6.7 | code_6.6 中的done block 回撥做了什麼 | SDWebImageManager.m_line:180-335 | 1,當前載入圖片operation是否被取消判斷。2,判斷是否要下載。3,下載使用SDWebImageDownloader執行下載方法並返回一個SDWebImageDownloadToken型別的downloadToken,這裡也有一個下載operation的回撥處理失敗和成功的事件 | 這裡捋一下查詢快取後的大步驟,接下來一步步分析。 |
code_7.1 | [self safelyRemoveOperationFromRunning:strongOperation]; | SDWebImageManager.m_line:182 | 當前operation不存在或者被取消,從執行佇列中刪除當前operation, code_6.4 的反向操作 | |
code_7.2 | [self.imageDownloader downloadImageWithURL:options:progress:completed:] | SDWebImageManager.m_line:222 | 開始下載,並將下載operation的token返回,當前載入程序強引用此token,它包含了當前的下載operation,url和用來取消時的token(此token其實是對下載進度和完成回撥的一個強引用) | |
code_7.3 | operation = [self createDownloaderOperationWithUrl:url options:options]; | SDWebImageDownloader.m_line:294 | 建立下載operation(SDWebImageDownloaderOperation) | 1,在這行程式碼前後都出現了URLOperations ,它是一個可變字典,用來維護url和operation之間的對應關係,可以說是儲存當前正在執行的下載operation2, SDWebImageDownloaderOperation 的下載過程?3,[Array removeObjectIdenticalTo:] API 的好處 |
code_7.4 | 對下載完成後的動作解析 | SDWebImageManager.m_line:224-335 | 下載operation的完成回撥處理過程:1,如果operation被取消,什麼都不做。2,如果出現錯誤,呼叫completion block回撥錯誤,並把URL儲存起來,用在code_6.3處.3,如果成功,從failedURLs記錄中刪除當前url(如果有的話).4,如果只重新整理快取,下載圖片位空,則什麼都不處理,5如果下載成功,並且實現了imageManager:transformDownloadedImage:withURL: 代理方法,則進行圖片轉換.6,再如果就只做圖片的序列化(如果實現了序列化方法),快取到記憶體、磁碟中.7,完成回撥8,執行緒安全的刪除載入圖片的operation |
這個流程比較長,但是程式碼比較好理解,沒有很高深的地方,需要注意幾個tip:1,快取到記憶體並且快取到磁碟(如果options中有SDWebImageCacheMemoryOnly就不會快取到磁碟).2,[Array removeObjectIdenticalTo:] API 的好處.3, SDWebImageDownloaderOperation 的內部實現解析 |
code_8 | 截至到code_7.4我們從code_6開始進入的SDWebImageManager載入圖片的過程就結束了,下邊我們來看載入完成之後的回撥操作 | |||
code_9 | dispatch_main_async_safe(callCompletedBlockClojure); | UIView+WebCache.m_line:138 | case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set OR case 1b: we got no image and the SDWebImageDelayPlaceholder is not set | 不多解釋 |
code_10 | SDWebImageNoParamsBlock callCompletedBlockClojure | UIView+WebCache.m_line:124 | 自動設定圖片,重新整理當前view | 重寫了setNeedsLayout方法,在裡邊區分MAC系統和iPhone系統 |
不解與收穫
@synchronized同步
在iOS中,這種同步機制是比較慢的。具體原因我們可以看MrPeak的一篇文章 使用這個同步鎖的時候要控制好粒度,儘可能的細,並且要注意被同步函式中巢狀呼叫函式。
@synchronized(self) {
do something
}
這種傳參self的,一定要慎重。因為很有可能這個類外部,也會把它的一個例項變數作為@synchronized的引數,這樣就會產生死鎖。
LOCK(lock) UNLOCK(lock)
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
lock:dispatch_semaphore_t
這個不用多說,常用的。
衍生問題:我們對比一下幾種iOS的鎖
文中也有測試的程式碼,看了一下,基本來說是比較客觀的,所以,我們用鎖,最注重一下效率,當然自旋鎖正如文中所描述的並不是絕對安全的,所以將其排除,推薦使用dispatch_semaphore
NSMapTable
這個類的用法幾乎和NSDictionary一樣,最大的優勢在於他可以方便的控制對value物件的強弱引用,而NSDictionary如果想實現弱引用,必須通過[NSValue valueWithNonretainedObject:]
在做一層轉換。
由NSMapTable衍生的問題:NSHashTable、NSPointerArray。
和NSMapTable的應用場景相似的還有對應的NSHashTable
,NSPointerArray
,同樣提供了物件記憶體管理方式。和我們經常使用幾個型別的對應關係是:
NSSet | -> | NSHashTable |
---|
NSArray | -> | NSPointerArray |
---|
NSDictionary | -> | NSMapTable |
---|
在做一些操作封裝,比如operation的時候,用這個型別去記錄operation的狀態是非常方便的,因為可以快速的形成弱引用,這樣就不用擔心後邊的記憶體釋放問題。
NSAssert
斷言,我們就不用過多解釋了,溫故一下,常用斷言有 NSParameterAssert 、 NSAssert 、 NSCAssert 、NSCparameterAssert。
注意:在TARGET->Build Setting->ENABLE_NS_ASSERTIONS,可以控制Debug,Release模式下是否生效,千萬不要讓Release生效,那樣線上及其不穩定,當然這個是預設不生效的。
我們來看一下NSAssert
是怎麼定義的
#define NSAssert(condition, desc) \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
_NSAssertBody((condition), (desc), 0, 0, 0, 0, 0) \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS
#endif
可見,核心是_NSAssertBody
#define _NSAssertBody(condition, desc, arg1, arg2, arg3, arg4, arg5) \
do { \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) { \
NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
__assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
[[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd object:self file:__assert_file__ \
lineNumber:__LINE__ description:(desc), (arg1), (arg2), (arg3), (arg4), (arg5)]; \
} \
__PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
2,第二層一個條件非空控制.
3,緊接著獲取當前檔案的路徑,有空提示.
4,呼叫NSAssertionHandler
的方法丟擲異常.
NSAssertionHandler
內部就兩個方法:
// 丟擲OC的異常
- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(nullable NSString *)format,... NS_FORMAT_FUNCTION(5,6);
// 丟擲C的異常
- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(nullable NSString *)format,... NS_FORMAT_FUNCTION(4,5);
這個類我們也可以用來重寫,達到一種既能捕獲異常,也可以保證程式正常執行的效果,設想,我們debug的時候,如果程式碼質量差,一會兒一個crash是不是很噁心。
繼承NSAssertionHandler建立TestAssertionHandler Class
//TestAssertionHandler.m
- (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(nullable NSString *)format,... {
NSLog(@"\n 當前方法 %@ \n 當前物件 %@ \n 當前檔案路徑 %@ \n 程式碼行數%li", NSStringFromSelector(selector), object, fileName, (long)line);
}
- (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format,... {
NSLog(@"\n 當前方法 (%@)\n 當前檔案路徑 %@ \n 程式碼行數%li", functionName, fileName, (long)line);
}
初始化物件,並加入到當前執行緒
每個執行緒都有它自己的NSAssertionHandler例項,並且會自動建立。
TestAssertionHandler *handler = [[TestAssertionHandler alloc] init];
[[[NSThread currentThread] threadDictionary] setValue:handler forKey:NSAssertionHandlerKey];
TEST
NSString *s = @"2";
NSAssert([s isEqualToString:@"12"], @"string == 123");
Log:
當前方法 sy:
當前物件 <AppDelegate: 0x6000008c1c60>
當前檔案路徑 /Users/WangXuesen/Desktop/TEST/TEST/AppDelegate.m
程式碼行數80
_______________________________
NSParameterAssert(nil);
Log:
當前方法 sy:
當前物件 <AppDelegate: 0x600002a90040>
當前檔案路徑 /Users/WangXuesen/Desktop/TEST/TEST/AppDelegate.m
程式碼行數76
NSCache
1,執行緒安全 2,記憶體告警時自動清理 3,可設定最大快取大小,超過自動回收,最早的最先釋放 4,可設定最大快取物件數量,預設沒有限制,超出同上。
各種Operation
這裡我們學習的主要是思想
1,單一原則,一種operation就專門做一件事情。 2,operation操作完成後注意被取消的情況處理. 3,對operation的管理、快取.