本文由 iMetalk 團(tuán)隊(duì)的成員 Lefe 完成匾寝,主要幫助讀者深入理解一個(gè)第三方庫(kù)搬葬。
本文不會(huì)教你咋么使用SD,而是要告訴你如何讀懂SD艳悔,掌握SD的原理及架構(gòu)急凰。可能猜年,你也看過(guò)別人的對(duì)SD的源碼解析抡锈,不過(guò) Lefe
上網(wǎng)看了一下,大部分都是以一種簡(jiǎn)單的方式介紹SD乔外。本文主要通過(guò)不同的角度來(lái)學(xué)習(xí)SD床三,主要從以下方面著手:
- 各個(gè)文件的作用是什么
- SD 使用的知識(shí)點(diǎn)總結(jié)
- SD 中的思想
- 時(shí)序圖
- SD類(lèi)圖
- 使用實(shí)例
- 總結(jié)
各個(gè)文件的作用是什么
擴(kuò)展文件( UIView + ... ):
這些文件讓使用者更簡(jiǎn)單的使用,基本是傻瓜式的杨幼,你可以在不懂 SD 的情況下寫(xiě)出高性能的圖片加載勿璃。這就是 SD 的優(yōu)點(diǎn)所在。
- UIView+WebCache.h
這個(gè)文件可以說(shuō)是其它視圖加載圖片的關(guān)鍵推汽,其它擴(kuò)展是基于 UIView 擴(kuò)展的基礎(chǔ)上,實(shí)現(xiàn)了視圖本身加載圖片的方式歧沪。它和 UIView+WebCacheOperation.h
配合使用歹撒。這個(gè)類(lèi)主要提供了加載圖片的方法和加載圖片時(shí)顯示的 Loading。
加載圖片的方法主要是:
- (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;
這個(gè)方法主要用來(lái)加載圖片诊胞,其實(shí) UIImageView
和 UIButton
加載圖片時(shí)最終會(huì)調(diào)用這個(gè)方法暖夭。這個(gè)方法會(huì)異步下載圖片并且添加緩存,這樣保證下次直接可以從緩存中讀取圖片撵孤。
參數(shù)說(shuō)明:
url
:圖片在服務(wù)器上的路徑迈着;
placeholder
:圖片加載時(shí)顯示的默認(rèn)圖;
options
:控制圖片的加載方式邪码,關(guān)于更多的 SDWebImageOptions 將在下文講解
operationKey
:操作(operation)的 key裕菠,如果為空時(shí),將使用類(lèi)名闭专。這個(gè)主要使用來(lái)取消一個(gè) opetion奴潘,結(jié)合 UIView+WebCacheOperation.h
使用;
setImageBlock
:如果不想使用 SD 加載完圖片后顯示到視圖上影钉,可以使用這個(gè) Block 自定義加載圖片画髓,這樣就可以在調(diào)用加載圖片的方法中加載圖片。它的完整定義是:
typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);
progress
:進(jìn)度回調(diào)平委,它的完整定義是奈虾,注意這里有一個(gè) targetURL:
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);
completed
:圖片加載完成后的回調(diào),
typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);
這里摘錄一段代碼,簡(jiǎn)單講解一些肉微,以下代碼主要用到的知識(shí)點(diǎn)有:
- 位運(yùn)算 &
- 使用 NSOperation 下載圖片
- 使用 runtime 給擴(kuò)展添加屬性
- 顯示加載 Loading
// 設(shè)置圖片時(shí)先取消以前的下載任務(wù)匾鸥,這樣避免了復(fù)用圖片錯(cuò)誤問(wèn)題
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 設(shè)置默認(rèn)圖
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
// 加載圖片
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 如果是自動(dòng)設(shè)置圖,直接回調(diào)出去
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 設(shè)置圖片
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
// 如果圖片加載失敗浪册,加載默認(rèn)圖
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 回調(diào)出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 保存當(dāng)前運(yùn)行的 operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
例子主要展示直接使用 UIView 的擴(kuò)展加載圖片扫腺,且使用 setImageBlock 加載圖片。只要理解了這個(gè)方法村象,那么關(guān)于 UIView 加載圖片基本上已經(jīng)掌握了:
[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
cell.sdimageView.image = image;
} progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
}];
- UIView+WebCacheOperation.h
這個(gè)類(lèi)主要用來(lái)記錄 UIView 加載 Operation 操作笆环,大多數(shù)情況下一個(gè) View 僅擁有
一個(gè) Operation ,默認(rèn)的 key 是當(dāng)前類(lèi)的類(lèi)名厚者,如果設(shè)置了不同的 key躁劣,將
保存不同個(gè) Operation 。比如一個(gè) UIButton库菲,可以設(shè)置不同狀態(tài)下的圖片账忘,那么我需要記錄多個(gè) Operation 。它主要采用一個(gè)字典來(lái)保存所有的 Operation 熙宇。
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
取消一個(gè) Operation鳖擒,這里需要注意 SDWebImageOperation。取消當(dāng)前正在進(jìn)行的 Operation烫止。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self operationDictionary];
id operations = operationDictionary[key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}
- UIImageView+WebCache.h
- UIImageView+HighlightedWebCache.h
- UIButton+WebCache.h
這幾個(gè)類(lèi)主要是基于以下方法的進(jìn)一步封裝蒋荚,方便實(shí)用,這里就不做介紹了馆蠕。
- (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;
- UIImage+GIF.h
主要用來(lái)根據(jù) NSData 生成一個(gè) GIF 圖片和一個(gè)判斷是否為 GIF 圖片期升。
- UIImage+MultiFormat.h
主要用來(lái)根據(jù) NSData 生成不同格式的圖片,這里可能我們需要用到的是互躬,根據(jù) Data 判斷圖片的格式播赁。
下載操作
- SDWebImageDownloaderOperation:NSOperation
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
這個(gè)文件可以說(shuō)是整個(gè) SD 的靈魂,它控制著圖片的下載過(guò)程吼渡,它與 NSOperationQueue 配合使用容为。關(guān)于更多 NSOperation 的介紹,近期會(huì)翻譯一篇文章來(lái)聊一聊 NSOperation寺酪。SDWebImageDownloaderOperationInterface:這是一個(gè)協(xié)議舟奠,可以自定義自己的 NSOperation,只要實(shí)現(xiàn)該協(xié)議中的方法房维,并且繼承自 NSOperation沼瘫。
主要用到的知識(shí)點(diǎn):
- 使用 NSURLSession 下載
- dispatch_barrier_async,dispatch_barrier_sync咙俩,dispatch_sync
- 自定義 NSOperation
- 網(wǎng)絡(luò)請(qǐng)求認(rèn)證
- 通知中心
- 后臺(tái)任務(wù)
初始化:
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;
使用這個(gè)方法來(lái)創(chuàng)建一個(gè) SDWebImageDownloaderOperation耿戚,NS_DESIGNATED_INITIALIZER 這個(gè)宏說(shuō)明所有的初始化方法最終都要調(diào)用這個(gè)方法湿故,request 就是網(wǎng)絡(luò)請(qǐng)求的 request,session 當(dāng)前 Operation 所在的 Session膜蛔,options:SDWebImageDownloaderOptions坛猪,如何來(lái)下載任務(wù),有一些枚舉值皂股。
SDWebImageDownloader
這個(gè)類(lèi)主要負(fù)責(zé)下載圖片墅茉,它是一個(gè)單例。它內(nèi)部有 SDWebImageDownloadToken
呜呐,用來(lái)標(biāo)示一個(gè)下載任務(wù)就斤,這樣根據(jù) token 來(lái)取消對(duì)應(yīng)的任務(wù)∧⒓可以使用以下方法對(duì) SDWebImageDownloader 進(jìn)行初始化洋机。當(dāng)然如果想使用一個(gè)自定義的 NSURLSessionConfiguration,可以使用下面這個(gè)初始化方法:
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
來(lái)初始化洋魂,下面是它的具體實(shí)現(xiàn):
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
// 下載的 Operation
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
// 下載對(duì)列绷旗,最大的并發(fā)數(shù)是6
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
// HTTP header
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
// NSURLSession
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
這是 SDWebImageDownloader 最終調(diào)用的初始化方法,主要配置了一些下載必備的數(shù)據(jù)副砍。
下載方法:這個(gè)方法主要用來(lái)下載一個(gè)任務(wù)衔肢,下載任務(wù)使用的是 NSOperation + NSOperationQueue,來(lái)控制下載豁翎。也就是說(shuō)這個(gè)方法主要生產(chǎn)一個(gè) NSOperation 角骤,并添加到 NSOperationQueue 中,這樣 NSOperationQueue 將自動(dòng)管理下載任務(wù)谨垃。使用 NSOperation 的好處就是可以控制下載的整個(gè)過(guò)程,并且不需要管理線程的創(chuàng)建硼控。當(dāng)然它的優(yōu)點(diǎn)也就是它的缺點(diǎn)刘陶,只是使用場(chǎng)景的不同。
url:圖片下載的路徑
options:圖片下載的選項(xiàng)牢撼,它主要有下面這幾種選項(xiàng):
- SDWebImageDownloaderLowPriority = 1 << 0, 低優(yōu)先級(jí)
- SDWebImageDownloaderProgressiveDownload = 1 << 1, 漸進(jìn)式的下載匙隔,也就是一塊一塊的下載
- SDWebImageDownloaderUseNSURLCache = 1 << 2, 默認(rèn)情況不使用 URLCache,它與 NSURLRequestUseProtocolCachePolicy 對(duì)應(yīng)熏版,設(shè)置后使用 URLCache
- SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
- SDWebImageDownloaderContinueInBackground = 1 << 4, 后臺(tái)下載任務(wù)
- SDWebImageDownloaderHandleCookies = 1 << 5, 它與 HTTPShouldHandleCookies 對(duì)應(yīng)
- SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, 允許不信任的 SSL 證書(shū)
- SDWebImageDownloaderHighPriority = 1 << 7, 高優(yōu)先級(jí)下載
- SDWebImageDownloaderScaleDownLargeImages = 1 << 8, 對(duì)下載后的圖片做處理
progress:進(jìn)度回調(diào)纷责,注意這個(gè)進(jìn)度是在后臺(tái)線程執(zhí)行,刷新 UI 需要回到主線程
completed:下載完成后的回調(diào)
SDWebImageDownloadToken:返回值用這個(gè)來(lái)標(biāo)示一個(gè)下載任務(wù)撼短,取消的時(shí)候使用
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// block 返回值是 SDWebImageDownloaderOperation再膳,在 block 中創(chuàng)建一個(gè) SDWebImageDownloaderOperation
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 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;
}
// 創(chuàng)建 SDWebImageDownloaderOperation,創(chuàng)建完成后添加到downloadQueue 中
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
// 處理 HTTP 認(rèn)證的曲横,大多情況不用處理
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
// 設(shè)置 Operation 的優(yōu)先級(jí)
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
使用上面這個(gè)方法下載時(shí)喂柒,前提需要了解下面這個(gè)方法的實(shí)現(xiàn)不瓶。它使用一個(gè)字典緩存了所有的下載。使用 SDWebImageDownloadToken 來(lái)標(biāo)記一個(gè)下載任務(wù)灾杰。
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
// 如果 URL 為空直接回調(diào)蚊丐,并返回
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
__block SDWebImageDownloadToken *token = nil;
dispatch_barrier_sync(self.barrierQueue, ^{
// 從緩存中取出 Operation
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
// 緩存不存在,調(diào)用 Block 創(chuàng)建一個(gè)新的 Operation
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
// 創(chuàng)建一個(gè)標(biāo)記艳吠,并添加回調(diào)到緩存
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
以上就是下載的主要方法麦备。還有一些設(shè)置屬性,很簡(jiǎn)單昭娩,這里不作介紹凛篙。
緩存 SDImageCache
SD中的緩存主要采用了內(nèi)存緩存(NSCache)加磁盤(pán)緩存(保存到沙河目錄中的 Cache 目錄下),SDImageCacheConfig 主要負(fù)責(zé)配置緩存题禀。
初始化
directory
:文件所要保存到沙河目錄鞋诗,默認(rèn)的是 Cache 目錄
ns
:文件的域名,最終的路徑為:.../cache/om.hackemist.SDWebImageCache.ns
迈嘹。需要注意的是所有的I/O操作都在一個(gè)串行對(duì)列中執(zhí)行削彬。這里主要用到了文件的一些操作,比如文件大小秀仲,保存文件融痛,文件路徑等。文件保存到沙盒時(shí)主要以文件的下載路徑神僵,MD5后雁刷,加上文件后綴作為文件名,保存到本地和 NSCache 中保礼。
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
它監(jiān)聽(tīng)了3個(gè)通知在初始化的時(shí)候:
- UIApplicationDidReceiveMemoryWarningNotification:有內(nèi)存警告時(shí)清除所有的緩存
- UIApplicationWillTerminateNotification:刪除已過(guò)期的文件
- UIApplicationDidEnterBackgroundNotification:在后臺(tái)刪除已過(guò)期的文件
當(dāng)然可以使用單例初始化沛励,使用默認(rèn)的配置。
+ (nonnull instancetype)sharedImageCache;
SDWebImageManager
主要用來(lái)管理 SDImageCache 和 SDWebImageDownloader炮障。也就是它把緩存和下載結(jié)合起來(lái)目派。
初始化:
這個(gè)方法是 SDWebImageManager 最終的初始化方法,也就是說(shuō)所有的初始化方法最終都會(huì)調(diào)用這個(gè)方法胁赢,方便使用者自定義 SDWebImageManager企蹭,當(dāng)然通常情況下使用單例方法初始化 + (nonnull instancetype)sharedManager;
。
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
_imageCache = cache;
_imageDownloader = downloader;
// 下載失敗的 URL 緩存智末,注意它使用的是集合谅摄,這樣保證緩存中沒(méi)有重復(fù)的 URL
_failedURLs = [NSMutableSet new];
// 正在運(yùn)行的操作
_runningOperations = [NSMutableArray new];
}
return self;
}
下載一個(gè)圖片的主要方法:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
這里會(huì)將方法分成很多部分來(lái)講:
- 1.參數(shù)異常判斷,保證程序的健壯性系馆,一個(gè)好的程序送漠,要處理好各種異常情況
// 使用斷言來(lái)保證完成的 Block 不能為空,也就是說(shuō)如果你不需要完成回調(diào)由蘑,直接使用 SDWebImagePrefetcher 就行
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 保證 URL 是 NSString 類(lèi)型螺男,轉(zhuǎn)換成 NSURL 類(lèi)型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 保證 url 為 NSURL 類(lèi)型
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
- 2.對(duì) url 做異常處理棒厘,是否為不可使用的下載鏈接。
SDWebImageCombinedOperation
是一個(gè) NSObeject 對(duì)象下隧。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 判斷是否為下載失敗的 url
BOOL isFailedUrl = NO;
if (url) {
// 保證線程安全
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
// 如果是失敗的 url 且 operations 不為 SDWebImageRetryFailed奢人,或者 url 為空直接返回錯(cuò)誤
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.保存當(dāng)前的 Operation 到緩存
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 獲取 url 對(duì)應(yīng)的 Key
NSString *key = [self cacheKeyForURL:url];
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
// typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);,cacheKeyFilter 是一個(gè) Block淆院,你可以自己設(shè)置 Cache 對(duì)應(yīng)的 key
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
- 從 Cache 中獲取圖片何乎,它結(jié)合 option,進(jìn)行不同的操作
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
- 4-1.如果 Operation 已經(jīng)取消土辩,則移除支救,并結(jié)束程序的執(zhí)行
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
- 4-2. 如果未能在緩存中找到圖片,或者強(qiáng)制刷新緩存拷淘,或者代理中未實(shí)現(xiàn)要強(qiáng)制下載圖片各墨,那么它就需要下載圖片。
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {}
SDWebImageDownloaderOptions 根據(jù)不同的選項(xiàng)做不同的操作启涯,根據(jù) SDWebImageOptions 轉(zhuǎn)換成對(duì)應(yīng)的 SDWebImageDownloaderOptions贬堵。這里需要注意位運(yùn)算,根據(jù)位運(yùn)算可以計(jì)算出不同的選項(xiàng)结洼。那么使用位定義的枚舉和用普通定義的枚舉值有什么優(yōu)缺點(diǎn)黎做?需要讀者考慮。比如下面這兩種定義方法個(gè)的優(yōu)缺點(diǎn)松忍。
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderLowPriority = 1,
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) {
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
使用 imageDownloader
下載圖片蒸殿,下載完成后保存到緩存,并移除 Operation鸣峭。如果發(fā)生錯(cuò)誤宏所,,需要將失敗的 Url 保存到 failedURLs摊溶,避免實(shí)效的 Url 多次下載爬骤。這里需要注意一個(gè) delegate ([self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]
),它需要調(diào)用者自己實(shí)現(xiàn)更扁,這樣緩存中將保存轉(zhuǎn)換后的圖片盖腕。
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished){
}
4-3. 在緩存中找到圖片赫冬,直接返回
4-4. 圖片不在緩存或者代理中不需要下載的浓镜,直接返回
SDWebImagePrefetcher
它是一個(gè)圖片預(yù)加載的類(lèi),你可以設(shè)置多個(gè) URL劲厌。這種更適合哪些膛薛,在 wifi 情況下提前加載一些圖片,緩存起來(lái)补鼻,用戶使用的時(shí)候哄啄,直接從本地緩存中讀取雅任。它實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單,使用一個(gè)遞歸來(lái)執(zhí)行每一個(gè)下載咨跌。它的本質(zhì)使用的是 SDWebImageManager 處理下載沪么,沒(méi)有使用單例,而新創(chuàng)建一個(gè) manager锌半。
初始化:
(nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager {
if ((self = [super init])) {
_manager = manager;
_options = SDWebImageLowPriority;
_prefetcherQueue = dispatch_get_main_queue();
self.maxConcurrentDownloads = 3;
}
return self;
}
SDWebImagePrefetcherDelegate:
每下載完一個(gè)后禽车,走一次回調(diào)
- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;
所有任務(wù)下載完后,執(zhí)行回調(diào)
- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount;
SDWebImageCompat
由于 SD 會(huì)用到不同的平臺(tái)刊殉,需要做一些兼容性的處理殉摔。
NSData+ImageContentType
根據(jù) Data 來(lái)解析圖片的格式
SD 使用的知識(shí)點(diǎn)總結(jié)
- GCD:
關(guān)于引用一段話:
Dispatch barriers 是一組函數(shù),在并發(fā)隊(duì)列上工作時(shí)扮演一個(gè)串行式的瓶頸记焊。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個(gè)特定時(shí)間上是指定隊(duì)列上唯一被執(zhí)行的條目逸月。這就意味著所有的先于調(diào)度障礙提交到隊(duì)列的條目必能在這個(gè) Block 執(zhí)行前完成。
// 創(chuàng)建一個(gè)并行隊(duì)列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 添加一個(gè)任務(wù)到對(duì)列中遍膜,使用 dispatch_barrier_async 添加的任務(wù)可以保存后添加
的任務(wù)依賴與前面添加過(guò)的任務(wù)碗硬,也就是說(shuō)如果先前添加的任務(wù)還沒(méi)有執(zhí)行完成,那么后添加
的任務(wù)不會(huì)執(zhí)行捌归,從而保證了線程安全肛响。
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
// dispatch_sync 保證同步執(zhí)行方法,保證了線程安全
- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
__block NSMutableArray<id> *callbacks = nil;
dispatch_sync(self.barrierQueue, ^{
callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
[callbacks removeObjectIdenticalTo:[NSNull null]];
});
return [callbacks copy];
}
// dispatch_barrier_sync 保證同步執(zhí)行方法惜索,保證了線程安全
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
// 回到主線程
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
// SD 的 cache 使用一個(gè)串行對(duì)列特笋,控制線程的訪問(wèn)
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
NSOperation:
使用 NSOperation 更好的控制一個(gè)邏輯復(fù)雜的操作,可以控制它的整個(gè)操作過(guò)程巾兆,同時(shí)也不需要自己管理和創(chuàng)建線程猎物。關(guān)于自定義 NSOperation,這里不做過(guò)多的解釋角塑。不過(guò)使用 NSOperation 可以做到 Operation 之間的依賴蔫磨,控制隊(duì)列中操作的最大并發(fā)數(shù),取消某個(gè)操作圃伶,而使用 GCD 的話做不到這一點(diǎn)堤如。NSURLSession:
這是 iOS7 以后網(wǎng)絡(luò)請(qǐng)求類(lèi),它可以支持文件上傳窒朋,文件下載搀罢。使用 runtime 給某個(gè)已有的類(lèi)添加屬性
static char TAG_ACTIVITY_STYLE;
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}
- (int)sd_getIndicatorStyle{
return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}
-
NSCache:
內(nèi)存緩存,如同字典一樣很好用侥猩。
SD 中的思想
- 耦合度低榔至,每個(gè)類(lèi)負(fù)責(zé)不同的操作,相互之間可以獨(dú)立使用
- 使用擴(kuò)展欺劳,方便使用者
- 異步下載圖片唧取,并保存到內(nèi)存與磁盤(pán)铅鲤,提高系統(tǒng)性能
- 保證主線程不被卡頓,提高性能
- 通過(guò)一個(gè) Manager 來(lái)控制不同的操作
時(shí)序圖
這張流程圖涵蓋了 SD 加載一張圖片時(shí)需要經(jīng)歷的過(guò)程:
SD類(lèi)圖
通過(guò)以上的學(xué)習(xí)枫弟,我們可以掌握各個(gè)類(lèi)的作用邢享,那么可以總結(jié)一下這張圖。
- 所有的操作都圍繞在 SDWebImageManager;
- SDWebImageManager 中包含了 SDImageCache 和 SDWebImageDownloader,來(lái)處理圖片的下載和緩存艘儒;
- SDWebImageDownloader 使用 SDWebImageDownloaderOperation 執(zhí)行下載操作;
- SDImageCache 使用 SDImageConfig 來(lái)配置緩存
- 從 SDWebImageManager 衍生出一個(gè)預(yù)加載圖片的類(lèi) SDWebImagePrefetcher绪爸,負(fù)責(zé)多個(gè)圖片的預(yù)先加載
- 底層封裝好通過(guò)擴(kuò)展 UIView 讓視圖可以加載圖片
看懂這張圖需要明白 UML(Unified Modeling Language) 類(lèi)圖:
- 依賴關(guān)系(dependency):
依賴關(guān)系是用一套帶箭頭的虛線表示的,UIButton(WebCache) 依賴于 UIView(WebCache)宙攻;
它是一種臨時(shí)性的關(guān)系奠货,通常在運(yùn)行期間產(chǎn)生,并且隨著運(yùn)行時(shí)的變化座掘; 依賴關(guān)系也可能發(fā)生變化.顯然递惋,依賴也有方向,雙向依賴是一種非常糟糕的結(jié)構(gòu)溢陪,我們總是應(yīng)該保持單向依賴萍虽,杜絕雙向依賴的產(chǎn)生;
聚合關(guān)系(aggregation):聚合關(guān)系用一條帶空心菱形箭頭的直線表示形真,聚合關(guān)系用于表示實(shí)體對(duì)象之間的關(guān)系杉编,表示整體由部分構(gòu)成的語(yǔ)義;例如一個(gè)部門(mén)由多個(gè)員工組成咆霜;SDWebImagePrefetcher 由 SDWebImageManager 組成邓馒;
實(shí)現(xiàn)關(guān)系(realize):實(shí)現(xiàn)關(guān)系用一條帶空心箭頭的虛線表示;比如 SDWebImageDownloaderOperation 實(shí)現(xiàn)了協(xié)議 SDWebImageOperation
泛化關(guān)系(generalization):泛化關(guān)系用一條帶空心箭頭的實(shí)線表示蛾坯,它是一種繼承關(guān)系光酣。
使用實(shí)例
- 實(shí)例一:使用 UIView 的擴(kuò)展加載圖片,并外部自動(dòng)設(shè)置圖片
[cell.sdimageView sd_internalSetImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:nil options:SDWebImageLowPriority operationKey:nil setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
cell.sdimageView.image = image;
} progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
}];
- 實(shí)例二:預(yù)加載圖片
[SDWebImagePrefetcher sharedImagePrefetcher].delegate = self;
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:resultUrl progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
}];
總結(jié)
通過(guò) SD 的深入學(xué)習(xí)脉课,讓我了解到一個(gè)好的開(kāi)源庫(kù)中使用的思想救军,深有體會(huì),建議讀者也可以嘗試詳細(xì)讀一個(gè)開(kāi)源庫(kù)倘零。在讀 SD 的時(shí)候唱遭,需要把自己不懂的知識(shí)點(diǎn),通過(guò)其它資料來(lái)掌握视事,這個(gè)過(guò)程收獲很大胆萧。前后大約花費(fèi)了一周的時(shí)間(每天 1小時(shí) 30 分庆揩,大約)俐东,完成了這篇博客跌穗,如果有什么不合理的地方,讀者可以指出虏辫。深知寫(xiě)博客需要一個(gè)長(zhǎng)期堅(jiān)持的過(guò)程蚌吸,而付出很多自由的時(shí)間。所以我在看別人的博客時(shí)會(huì)特別認(rèn)真的融入作者當(dāng)時(shí)的思想中砌庄。那么 SD 中的思想究竟如何運(yùn)用到我們的項(xiàng)目中呢羹唠?lefe 建議讀者可以從以下方面入手:
- 解耦:模塊之間一定不要有太多的關(guān)聯(lián),我們往往對(duì)項(xiàng)目中的某個(gè)類(lèi)做增量操作娄昆,不斷的給某個(gè)類(lèi)添加新的代碼佩微,導(dǎo)致這個(gè)類(lèi)越來(lái)越重,我們?cè)囍岩粋€(gè)類(lèi)拆分為不同的功能模塊萌焰;
- 思路明確:從圖片的下載到圖片顯示到視圖上哺眯,要有明確的思路,先有一個(gè)大致的流程扒俯,然后逐步細(xì)化奶卓,逐步實(shí)現(xiàn);
- 層次明確:應(yīng)用層的使用不會(huì)印象到底層的設(shè)計(jì)撼玄;
- GCD 和 NSOperation: 各有利弊夺姑,要合理的使用;
- 注意性能:一定要注意性能掌猛,結(jié)合多線程盏浙,提升性能,比如 SD 讀取文件時(shí)會(huì)在一條線程中讀壤蟛纭只盹;
- 方便使用者:寫(xiě)三方庫(kù)時(shí),要讓用戶使用起來(lái)超級(jí)方便兔院,比如在自己項(xiàng)目中寫(xiě)項(xiàng)目組中公用的模塊時(shí)殖卑,要有明確的注釋?zhuān)屖褂眠@更方便的使用;
參考
如果您想第一時(shí)間看到我們的文章坊萝,歡迎關(guān)注公眾號(hào)孵稽。
===== 我是有底線的 ======
喜歡我的文章,歡迎關(guān)注我的新浪微博 Lefe_x十偶,我會(huì)不定期的分享一些開(kāi)發(fā)技巧