1. 程式人生 > >SDWebImage原始碼中閱讀總結-那些不解和收穫

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的管理、快取.