前言
因為對大神的開源代碼非常崇拜修陡,所以開始學(xué)習(xí)這些開源的代碼媒抠。
這是一年前學(xué)習(xí)源碼時寫在印象筆記里的筆記,過了一年官硝,今天又把最新的SDWebImage下下來看,發(fā)現(xiàn)沒什么大變化短蜕。
分析
1.我們平時開發(fā),用的最多的就是:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
這一系列方法.使用非常簡單:
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder"]];
2.那這個方法是怎么實現(xiàn)的呢?
a.這個方法在UIImageView+WebCache分類中, 有一系列.當(dāng)最終這些方法都會調(diào)用下面這個.區(qū)別只是傳的參數(shù),要么傳了空,要么是默認(rèn)值.
/**
<# 平時用的方法,只是對這個方法包裝了一下 #>
@param url <# 網(wǎng)絡(luò)圖片鏈接 #>
@param placeholder <# 占位圖 #>
@param options <# 一個特殊的標(biāo)記 #>
@param progressBlock <# 進(jìn)度回調(diào)block #>
@param completedBlock <# 網(wǎng)絡(luò)圖像加載完成回調(diào)的block #>
*/
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
b.上面這個方法又調(diào)用了UIView+WebCache分類的方法:
/**
<# 根據(jù)指定的url,異步下載圖像,并緩存.設(shè)置imageView中的image#>
@param url <#url 指定的url#>
@param placeholder <#placeholder 展占圖#>
@param options <#options 一個特殊的標(biāo)記#>
@param operationKey <#operationKey 下載操作(operation)的key值,默認(rèn)為類名#>
@param setImageBlock <#setImageBlock 自定義設(shè)置圖像的代碼 #>
@param progressBlock <#progressBlock 下載過程中進(jìn)度回調(diào)的block #>
@param completedBlock <#completedBlock 下載完成回調(diào)的block #>
*/
- (void)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;
3.現(xiàn)在對這個方法的代碼一行行分析:
3.1
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
a氢架、數(shù)據(jù)校驗,如果設(shè)置了operation的key就使用設(shè)置的,默認(rèn)使用類名作為key
b、根據(jù)operationKey找到對應(yīng)的操作,從下載隊列取消正在下載的操作
c忿危、使當(dāng)前 UIImageView 中的所有操作都被 cancel. 不會影響之后進(jìn)行的下載操作.
d达箍、這里只是說了,不取消,會影響后面的下載操作.那為什么會影響?影響的結(jié)果是什么?
為什么會影響:
因為UIImageView一個對象應(yīng)該只對應(yīng)的是一個下載操作,如果UIImageView的當(dāng)前對象,他的前一個下載操作未完成,然后又來了一個下載操作(同一個imageView多次調(diào)用了這個方法),這樣就有了2個下載操作了.這不符合一個UIImageView對象對應(yīng)一個下載操作.
影響的結(jié)果:
當(dāng)前一個下載操作將圖像請求完成,展示到imageView上,過一會后面的操作也請求完成,這樣又會再一次設(shè)置imageview展示的圖像.雖然這可能不會有太大的影響,但是至少是消耗性能了,做了沒必要的操作.
3.2 利用runtime,對url做一次retain
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
3.3
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
a.如果傳入的 options 中沒有 SDWebImageDelayPlaceholder(默認(rèn)情況下 options == 0),
b.那么就會為 UIImageView 添加一個臨時的 image, 也就是占位圖.
c.SDWebImageDelayPlaceholder(我的理解就是,不讓用占位圖),取反就是允許使用占位圖
3.4
//檢查url不為空才去加載
if (url) {
}
//否則直接調(diào)用加載完成回調(diào)的block,并吧錯誤傳入block
else{
}
//檢查activityView(指示器)是否可用
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView])
[selfsd_addActivityIndicator];
}
3.5調(diào)用 SDWebImageManager的對象方法去獲取圖像
/**
<# 指定的url如果沒有緩沖,就去下載圖像 #>
@param url <#url 指定的url#>
@param options <#options 一個特殊的標(biāo)記#>
@param progressBlock <#progressBlock 下載過程中進(jìn)度回調(diào)的block#>
@param completedBlock <#completedBlock 下載完成回調(diào)的block#>
@return <#return value 下載操作#>
*/
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
//在這個方法中,首先會驗證url的有效性
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
3.6創(chuàng)建了一個包裝了一個看著像下載操作的卻并不是下載操作的類SDWebImageCombinedOperation,
這個類本身不是操作類,只是他內(nèi)部有一個NSOperation屬性(注意:在這里,operation屬性還沒有被賦值,是nil).當(dāng)然這個類還遵循了一個協(xié)議,SDWebImageOperation.這個協(xié)議只有一個取消的方法-cancel.
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
3.7去黑名單匹配當(dāng)前url是否是下載失敗過的url
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
3.8如果url驗證不通過,直接調(diào)用加載完成的回調(diào)block,并且傳error出去
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
return operation;
}
3.9準(zhǔn)備下載,首先將下載操作添加到數(shù)組保存起來
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
3.10根據(jù)url獲取到緩存的key
NSString *key = [self cacheKeyForURL:url];
//在正真去網(wǎng)上下載數(shù)據(jù)之前,調(diào)用SDImageCache的對象方法,
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
3.11在這個方法中,首先會驗證緩存的key的有效性.無效,說明緩存中沒有找到對應(yīng)的圖像;調(diào)用block去下載;
if (!key) {
if (doneBlock) { //標(biāo)記好無緩存
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
3.12查詢內(nèi)存緩存中是否能找到對應(yīng)的image,如果找到了,將image放doneBlock中傳出去,如果沒找到,繼續(xù)往后走
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
3.13然后再在磁盤緩存中查找,是否有對應(yīng)的image,在這,不管找沒找到,都將獲取diskImage(沒找到時為nil)放doneBlock中傳出去.最后返回operation.
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if (doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
3.14現(xiàn)在開始分析doneBlock中做寫什么.當(dāng)從內(nèi)存緩存,磁盤緩存查找回來,就開始執(zhí)行這個block
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
在這個block中,首先判斷剛才創(chuàng)建的operation是否已經(jīng)取消
if (operation.isCancelled) {
//將已經(jīng)取消的操作,從正在執(zhí)行的操作數(shù)組中移除
[self safelyRemoveOperationFromRunning:operation];
return;
}
//如果(
(`圖片不存在` || `options包含SDWebImageRefreshCached(刷新緩存)`)
&& 不能響應(yīng)imageManager:shouldDownloadImageForURL:方法)
||(或者)
(imageManager:shouldDownloadImageForURL:返回值為YES( 沒有緩存過的圖片將不會下載))
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]))
//如果在緩存中找到了image,但是需要刷新緩存,嘗試重新下載
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
3.16 如果圖片不存在,或者設(shè)置了SDWebImageRefreshCached 需要刷新緩存,就在下載
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
3.17調(diào)用 SDWebImageDownloader(下載器)的對象方法開始從網(wǎng)絡(luò)下載圖片:
/**
<# 根據(jù)url創(chuàng)建一個異步下載器實例,當(dāng)下載完成或下載失敗報錯時會通知代理 #>
@param url <#url 指定的url#>
@param options <#options 標(biāo)記 #>
@param progressBlock <# 進(jìn)度回調(diào)block #>
@param completedBlock <# 網(wǎng)絡(luò)圖像加載完成回調(diào)的block #>
@return <#return value description#>
*/
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
3.18然后在上面的方法中,會立即調(diào)用另外一個方法并返回
/**
<#Description#>
@param progressBlock <#progressBlock 進(jìn)度回調(diào)的block #>
@param completedBlock <#completedBlock 加載完畢回調(diào)的block #>
@param url <#url 指定圖像的url #>
@param createCallback <#createCallback 創(chuàng)建請求的block #>
@return <#return value description#>
*/
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback
3.19在這個方法中,首先驗證url不能為nil,因為后面保存callblock(回調(diào)的block)需要url作為key
// The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
3.20
//dispatch_barrier_sync:在前面的任務(wù)執(zhí)行結(jié)束后它才執(zhí)行,而且它后面的任務(wù)等它執(zhí)行完成之后才會執(zhí)行
dispatch_barrier_sync(self.barrierQueue, ^{
//根據(jù)url作為key去字典中獲取對應(yīng)的operation
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
//如果獲取的operation不存在,說明是第一次加載,需要回調(diào)createCallback,以保證請求被創(chuàng)建好
operation = createCallback();
//保存好operation
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
//這個block,是在請求執(zhí)行完成時回調(diào)
//當(dāng)請求執(zhí)行完畢,需要將URLOperations中保存的operation移除
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
//在這調(diào)用operation的對象方法,把progressBlock和completedBlock以字典的形式保存起來,并返回.
//讓downloadOperationCancelToken接收這個字典,所以downloadOperationCancelToken其實是一個可變字典
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
//與每個下載相關(guān)的令牌铺厨《忻担可以用來取消下載
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
然后返回這個token(令牌).
//看到這里,似乎我們根本就沒看見請求在哪.其實,前面有提到,當(dāng)根據(jù)url去獲取operation為空時,會回調(diào)一個createCallback
//對,就是在createCallback中創(chuàng)建了請求,并開始下載的.
//下面看看這個block中的代碼
//首先會設(shè)置超時時間,默認(rèn)15s
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 為防止重復(fù)緩存(NSURLCache + SDImageCache),如果設(shè)置了 SDWebImageDownloaderUseNSURLCache解滓,則禁用 SDImageCache
這個 request 就用于在之后發(fā)送 HTTP 請求.
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
//在初始化了這個 request 之后, 又初始化了一個 SDWebImageDownloaderOperation 的實例,
//這個實例, 就是用于請求網(wǎng)絡(luò)資源的操作. 它是一個 NSOperation 的子類,
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
// 設(shè)置 https 訪問時身份驗證使用的憑據(jù)
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
//將操作添加到隊列
//只有將它加入到這個下載隊列中, 這個操作才會執(zhí)行.也就是說operation 的 `start` 方法會被執(zhí)行.開始請求網(wǎng)路下載圖像
[sself.downloadQueue addOperation:operation];
//現(xiàn)在開始分析operation 的 `start` 方法里面做了些什么
//整個下載都是加鎖的,安全
@synchronized (self) {...}
//如果操作已經(jīng)取消,設(shè)置已完成標(biāo)記,重置操作
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
//下面這段代碼,最主要的就是,保證程序進(jìn)入后臺,也能保證app中的線程繼續(xù)工作
//beginBackgroundTaskWithExpirationHandler只要調(diào)用了此函數(shù)系統(tǒng)就會允許app的所有線程繼續(xù)執(zhí)行赃磨,直到任務(wù)結(jié)束(1,
[[UIApplicationsharedApplication]backgroundTimeRemaining] 的時間結(jié)束 2,調(diào)用endBackgroundTask)
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
//如果在系統(tǒng)規(guī)定的時間內(nèi)任務(wù)還沒有完成,在時間到之前會調(diào)用這個block,一般是10分鐘
__strong__typeof(wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
3.21創(chuàng)建session
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
3.22
//啟動任務(wù),到這里我們用到的UIImageVIew+WebCache里面的方法sd_ImageWithURL:這系列方法是如何達(dá)到效果的就分析完畢了
[self.dataTask resume];
3.23啟動任務(wù)之后,調(diào)用progressBlock,發(fā)送開始下載的通知
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
3.24這個方法的操作可以保證新的操作隊列不會被舊的影響,同時把該清理的狀態(tài)都?xì)w為完畢
//任務(wù)完成洼裤,處理釋放對象
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}