SDWebImage源碼中閱讀總結-那些不解和收穫

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之間的對應關係,可以說是存儲當前正在執行的下載operation
2, 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的管理、緩存.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章