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的鎖
根據ibireme寫的不再安全的 OSSpinLock一文所做的測試如圖(圖片也來自這篇文章):
文中也有測試的代碼,看了一下,基本來說是比較客觀的,所以,我們用鎖,最注重一下效率,當然自旋鎖正如文中所描述的並不是絕對安全的,所以將其排除,推薦使用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)
1,最外層的do-while(0),這是經典的宏定義寫法,不理解的話看這篇文章:《do{…}while(0)的妙用》作者:IvanRunning
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的管理、緩存.