本文由 iMetalk 團隊的成員 Lefe 完成,主要幫助讀者深入理解一個第三方庫。
本文不會教你咋么使用SD,而是要告訴你如何讀懂SD,掌握SD的原理及架構。可能,你也看過別人的對SD的源碼解析,不過 Lefe
上網(wǎng)看了一下,大部分都是以一種簡單的方式介紹SD。本文主要通過不同的角度來學習SD,主要從以下方面著手:
- 各個文件的作用是什么
- SD 使用的知識點總結
- SD 中的思想
- 時序圖
- SD類圖
- 使用實例
- 總結
各個文件的作用是什么
擴展文件( UIView + ... ):
這些文件讓使用者更簡單的使用,基本是傻瓜式的,你可以在不懂 SD 的情況下寫出高性能的圖片加載。這就是 SD 的優(yōu)點所在。
- UIView+WebCache.h
這個文件可以說是其它視圖加載圖片的關鍵,其它擴展是基于 UIView 擴展的基礎上,實現(xiàn)了視圖本身加載圖片的方式。它和 UIView+WebCacheOperation.h
配合使用。這個類主要提供了加載圖片的方法和加載圖片時顯示的 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;
這個方法主要用來加載圖片,其實 UIImageView
和 UIButton
加載圖片時最終會調用這個方法。這個方法會異步下載圖片并且添加緩存,這樣保證下次直接可以從緩存中讀取圖片。
參數(shù)說明:
url
:圖片在服務器上的路徑;
placeholder
:圖片加載時顯示的默認圖;
options
:控制圖片的加載方式,關于更多的 SDWebImageOptions 將在下文講解
operationKey
:操作(operation)的 key,如果為空時,將使用類名。這個主要使用來取消一個 opetion,結合 UIView+WebCacheOperation.h
使用;
setImageBlock
:如果不想使用 SD 加載完圖片后顯示到視圖上,可以使用這個 Block 自定義加載圖片,這樣就可以在調用加載圖片的方法中加載圖片。它的完整定義是:
typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData);
progress
:進度回調,它的完整定義是,注意這里有一個 targetURL:
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);
completed
:圖片加載完成后的回調,
typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);
這里摘錄一段代碼,簡單講解一些,以下代碼主要用到的知識點有:
- 位運算 &
- 使用 NSOperation 下載圖片
- 使用 runtime 給擴展添加屬性
- 顯示加載 Loading
// 設置圖片時先取消以前的下載任務,這樣避免了復用圖片錯誤問題
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
// 設置默認圖
[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) {
// 如果是自動設置圖,直接回調出去
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 設置圖片
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
// 如果圖片加載失敗,加載默認圖
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 回調出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 保存當前運行的 operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
}
例子主要展示直接使用 UIView 的擴展加載圖片,且使用 setImageBlock 加載圖片。只要理解了這個方法,那么關于 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
這個類主要用來記錄 UIView 加載 Operation 操作,大多數(shù)情況下一個 View 僅擁有
一個 Operation ,默認的 key 是當前類的類名,如果設置了不同的 key,將
保存不同個 Operation 。比如一個 UIButton,可以設置不同狀態(tài)下的圖片,那么我需要記錄多個 Operation 。它主要采用一個字典來保存所有的 Operation 。
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
取消一個 Operation,這里需要注意 SDWebImageOperation。取消當前正在進行的 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
這幾個類主要是基于以下方法的進一步封裝,方便實用,這里就不做介紹了。
- (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
主要用來根據(jù) NSData 生成一個 GIF 圖片和一個判斷是否為 GIF 圖片。
- UIImage+MultiFormat.h
主要用來根據(jù) NSData 生成不同格式的圖片,這里可能我們需要用到的是,根據(jù) Data 判斷圖片的格式。
下載操作
- SDWebImageDownloaderOperation:NSOperation
@interface SDWebImageDownloaderOperation : NSOperation <SDWebImageDownloaderOperationInterface, SDWebImageOperation, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
這個文件可以說是整個 SD 的靈魂,它控制著圖片的下載過程,它與 NSOperationQueue 配合使用。關于更多 NSOperation 的介紹,近期會翻譯一篇文章來聊一聊 NSOperation。SDWebImageDownloaderOperationInterface:這是一個協(xié)議,可以自定義自己的 NSOperation,只要實現(xiàn)該協(xié)議中的方法,并且繼承自 NSOperation。
主要用到的知識點:
- 使用 NSURLSession 下載
- dispatch_barrier_async,dispatch_barrier_sync,dispatch_sync
- 自定義 NSOperation
- 網(wǎng)絡請求認證
- 通知中心
- 后臺任務
初始化:
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options NS_DESIGNATED_INITIALIZER;
使用這個方法來創(chuàng)建一個 SDWebImageDownloaderOperation,NS_DESIGNATED_INITIALIZER 這個宏說明所有的初始化方法最終都要調用這個方法,request 就是網(wǎng)絡請求的 request,session 當前 Operation 所在的 Session,options:SDWebImageDownloaderOptions,如何來下載任務,有一些枚舉值。
SDWebImageDownloader
這個類主要負責下載圖片,它是一個單例。它內部有 SDWebImageDownloadToken
,用來標示一個下載任務,這樣根據(jù) token 來取消對應的任務。可以使用以下方法對 SDWebImageDownloader 進行初始化。當然如果想使用一個自定義的 NSURLSessionConfiguration,可以使用下面這個初始化方法:
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
來初始化,下面是它的具體實現(xiàn):
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
// 下載的 Operation
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
// 下載對列,最大的并發(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 最終調用的初始化方法,主要配置了一些下載必備的數(shù)據(jù)。
下載方法:這個方法主要用來下載一個任務,下載任務使用的是 NSOperation + NSOperationQueue,來控制下載。也就是說這個方法主要生產(chǎn)一個 NSOperation ,并添加到 NSOperationQueue 中,這樣 NSOperationQueue 將自動管理下載任務。使用 NSOperation 的好處就是可以控制下載的整個過程,并且不需要管理線程的創(chuàng)建。當然它的優(yōu)點也就是它的缺點,只是使用場景的不同。
url:圖片下載的路徑
options:圖片下載的選項,它主要有下面這幾種選項:
- SDWebImageDownloaderLowPriority = 1 << 0, 低優(yōu)先級
- SDWebImageDownloaderProgressiveDownload = 1 << 1, 漸進式的下載,也就是一塊一塊的下載
- SDWebImageDownloaderUseNSURLCache = 1 << 2, 默認情況不使用 URLCache,它與 NSURLRequestUseProtocolCachePolicy 對應,設置后使用 URLCache
- SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
- SDWebImageDownloaderContinueInBackground = 1 << 4, 后臺下載任務
- SDWebImageDownloaderHandleCookies = 1 << 5, 它與 HTTPShouldHandleCookies 對應
- SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, 允許不信任的 SSL 證書
- SDWebImageDownloaderHighPriority = 1 << 7, 高優(yōu)先級下載
- SDWebImageDownloaderScaleDownLargeImages = 1 << 8, 對下載后的圖片做處理
progress:進度回調,注意這個進度是在后臺線程執(zhí)行,刷新 UI 需要回到主線程
completed:下載完成后的回調
SDWebImageDownloadToken:返回值用這個來標示一個下載任務,取消的時候使用
- (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)建一個 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 認證的,大多情況不用處理
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
// 設置 Operation 的優(yōu)先級
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;
}];
}
使用上面這個方法下載時,前提需要了解下面這個方法的實現(xiàn)。它使用一個字典緩存了所有的下載。使用 SDWebImageDownloadToken 來標記一個下載任務。
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
// 如果 URL 為空直接回調,并返回
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) {
// 緩存不存在,調用 Block 創(chuàng)建一個新的 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)建一個標記,并添加回調到緩存
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
以上就是下載的主要方法。還有一些設置屬性,很簡單,這里不作介紹。
緩存 SDImageCache
SD中的緩存主要采用了內存緩存(NSCache)加磁盤緩存(保存到沙河目錄中的 Cache 目錄下),SDImageCacheConfig 主要負責配置緩存。
初始化
directory
:文件所要保存到沙河目錄,默認的是 Cache 目錄
ns
:文件的域名,最終的路徑為:.../cache/om.hackemist.SDWebImageCache.ns
。需要注意的是所有的I/O操作都在一個串行對列中執(zhí)行。這里主要用到了文件的一些操作,比如文件大小,保存文件,文件路徑等。文件保存到沙盒時主要以文件的下載路徑,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)聽了3個通知在初始化的時候:
- UIApplicationDidReceiveMemoryWarningNotification:有內存警告時清除所有的緩存
- UIApplicationWillTerminateNotification:刪除已過期的文件
- UIApplicationDidEnterBackgroundNotification:在后臺刪除已過期的文件
當然可以使用單例初始化,使用默認的配置。
+ (nonnull instancetype)sharedImageCache;
SDWebImageManager
主要用來管理 SDImageCache 和 SDWebImageDownloader。也就是它把緩存和下載結合起來。
初始化:
這個方法是 SDWebImageManager 最終的初始化方法,也就是說所有的初始化方法最終都會調用這個方法,方便使用者自定義 SDWebImageManager,當然通常情況下使用單例方法初始化 + (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 緩存,注意它使用的是集合,這樣保證緩存中沒有重復的 URL
_failedURLs = [NSMutableSet new];
// 正在運行的操作
_runningOperations = [NSMutableArray new];
}
return self;
}
下載一個圖片的主要方法:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
這里會將方法分成很多部分來講:
- 1.參數(shù)異常判斷,保證程序的健壯性,一個好的程序,要處理好各種異常情況
// 使用斷言來保證完成的 Block 不能為空,也就是說如果你不需要完成回調,直接使用 SDWebImagePrefetcher 就行
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// 保證 URL 是 NSString 類型,轉換成 NSURL 類型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 保證 url 為 NSURL 類型
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
- 2.對 url 做異常處理,是否為不可使用的下載鏈接。
SDWebImageCombinedOperation
是一個 NSObeject 對象。
__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 為空直接返回錯誤
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.保存當前的 Operation 到緩存
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 獲取 url 對應的 Key
NSString *key = [self cacheKeyForURL:url];
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
// typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);,cacheKeyFilter 是一個 Block,你可以自己設置 Cache 對應的 key
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
- 從 Cache 中獲取圖片,它結合 option,進行不同的操作
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
- 4-1.如果 Operation 已經(jīng)取消,則移除,并結束程序的執(zhí)行
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
- 4-2. 如果未能在緩存中找到圖片,或者強制刷新緩存,或者代理中未實現(xiàn)要強制下載圖片,那么它就需要下載圖片。
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {}
SDWebImageDownloaderOptions 根據(jù)不同的選項做不同的操作,根據(jù) SDWebImageOptions 轉換成對應的 SDWebImageDownloaderOptions。這里需要注意位運算,根據(jù)位運算可以計算出不同的選項。那么使用位定義的枚舉和用普通定義的枚舉值有什么優(yōu)缺點?需要讀者考慮。比如下面這兩種定義方法個的優(yōu)缺點。
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ā)生錯誤,,需要將失敗的 Url 保存到 failedURLs,避免實效的 Url 多次下載。這里需要注意一個 delegate ([self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url]
),它需要調用者自己實現(xià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
它是一個圖片預加載的類,你可以設置多個 URL。這種更適合哪些,在 wifi 情況下提前加載一些圖片,緩存起來,用戶使用的時候,直接從本地緩存中讀取。它實現(xiàn)起來也很簡單,使用一個遞歸來執(zhí)行每一個下載。它的本質使用的是 SDWebImageManager 處理下載,沒有使用單例,而新創(chuàng)建一個 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:
每下載完一個后,走一次回調
- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount;
所有任務下載完后,執(zhí)行回調
- (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount;
SDWebImageCompat
由于 SD 會用到不同的平臺,需要做一些兼容性的處理。
NSData+ImageContentType
根據(jù) Data 來解析圖片的格式
SD 使用的知識點總結
- GCD:
關于引用一段話:
Dispatch barriers 是一組函數(shù),在并發(fā)隊列上工作時扮演一個串行式的瓶頸。使用 GCD 的障礙(barrier)API 確保提交的 Block 在那個特定時間上是指定隊列上唯一被執(zhí)行的條目。這就意味著所有的先于調度障礙提交到隊列的條目必能在這個 Block 執(zhí)行前完成。
// 創(chuàng)建一個并行隊列
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 添加一個任務到對列中,使用 dispatch_barrier_async 添加的任務可以保存后添加
的任務依賴與前面添加過的任務,也就是說如果先前添加的任務還沒有執(zhí)行完成,那么后添加
的任務不會執(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 使用一個串行對列,控制線程的訪問
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
NSOperation:
使用 NSOperation 更好的控制一個邏輯復雜的操作,可以控制它的整個操作過程,同時也不需要自己管理和創(chuàng)建線程。關于自定義 NSOperation,這里不做過多的解釋。不過使用 NSOperation 可以做到 Operation 之間的依賴,控制隊列中操作的最大并發(fā)數(shù),取消某個操作,而使用 GCD 的話做不到這一點。NSURLSession:
這是 iOS7 以后網(wǎng)絡請求類,它可以支持文件上傳,文件下載。使用 runtime 給某個已有的類添加屬性
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:
內存緩存,如同字典一樣很好用。
SD 中的思想
- 耦合度低,每個類負責不同的操作,相互之間可以獨立使用
- 使用擴展,方便使用者
- 異步下載圖片,并保存到內存與磁盤,提高系統(tǒng)性能
- 保證主線程不被卡頓,提高性能
- 通過一個 Manager 來控制不同的操作
時序圖
這張流程圖涵蓋了 SD 加載一張圖片時需要經(jīng)歷的過程:
SD類圖
通過以上的學習,我們可以掌握各個類的作用,那么可以總結一下這張圖。
- 所有的操作都圍繞在 SDWebImageManager;
- SDWebImageManager 中包含了 SDImageCache 和 SDWebImageDownloader,來處理圖片的下載和緩存;
- SDWebImageDownloader 使用 SDWebImageDownloaderOperation 執(zhí)行下載操作;
- SDImageCache 使用 SDImageConfig 來配置緩存
- 從 SDWebImageManager 衍生出一個預加載圖片的類 SDWebImagePrefetcher,負責多個圖片的預先加載
- 底層封裝好通過擴展 UIView 讓視圖可以加載圖片
看懂這張圖需要明白 UML(Unified Modeling Language) 類圖:
- 依賴關系(dependency):
依賴關系是用一套帶箭頭的虛線表示的,UIButton(WebCache) 依賴于 UIView(WebCache);
它是一種臨時性的關系,通常在運行期間產(chǎn)生,并且隨著運行時的變化; 依賴關系也可能發(fā)生變化.顯然,依賴也有方向,雙向依賴是一種非常糟糕的結構,我們總是應該保持單向依賴,杜絕雙向依賴的產(chǎn)生;
聚合關系(aggregation):聚合關系用一條帶空心菱形箭頭的直線表示,聚合關系用于表示實體對象之間的關系,表示整體由部分構成的語義;例如一個部門由多個員工組成;SDWebImagePrefetcher 由 SDWebImageManager 組成;
實現(xiàn)關系(realize):實現(xiàn)關系用一條帶空心箭頭的虛線表示;比如 SDWebImageDownloaderOperation 實現(xiàn)了協(xié)議 SDWebImageOperation
泛化關系(generalization):泛化關系用一條帶空心箭頭的實線表示,它是一種繼承關系。
使用實例
- 實例一:使用 UIView 的擴展加載圖片,并外部自動設置圖片
[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) {
}];
- 實例二:預加載圖片
[SDWebImagePrefetcher sharedImagePrefetcher].delegate = self;
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:resultUrl progress:^(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls) {
} completed:^(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls) {
}];
總結
通過 SD 的深入學習,讓我了解到一個好的開源庫中使用的思想,深有體會,建議讀者也可以嘗試詳細讀一個開源庫。在讀 SD 的時候,需要把自己不懂的知識點,通過其它資料來掌握,這個過程收獲很大。前后大約花費了一周的時間(每天 1小時 30 分,大約),完成了這篇博客,如果有什么不合理的地方,讀者可以指出。深知寫博客需要一個長期堅持的過程,而付出很多自由的時間。所以我在看別人的博客時會特別認真的融入作者當時的思想中。那么 SD 中的思想究竟如何運用到我們的項目中呢?lefe 建議讀者可以從以下方面入手:
- 解耦:模塊之間一定不要有太多的關聯(lián),我們往往對項目中的某個類做增量操作,不斷的給某個類添加新的代碼,導致這個類越來越重,我們試著把一個類拆分為不同的功能模塊;
- 思路明確:從圖片的下載到圖片顯示到視圖上,要有明確的思路,先有一個大致的流程,然后逐步細化,逐步實現(xiàn);
- 層次明確:應用層的使用不會印象到底層的設計;
- GCD 和 NSOperation: 各有利弊,要合理的使用;
- 注意性能:一定要注意性能,結合多線程,提升性能,比如 SD 讀取文件時會在一條線程中讀取;
- 方便使用者:寫三方庫時,要讓用戶使用起來超級方便,比如在自己項目中寫項目組中公用的模塊時,要有明確的注釋,讓使用這更方便的使用;
參考
如果您想第一時間看到我們的文章,歡迎關注公眾號。
===== 我是有底線的 ======
喜歡我的文章,歡迎關注我的新浪微博 Lefe_x,我會不定期的分享一些開發(fā)技巧