版本記錄
| 版本號(hào) | 時(shí)間 |
|---|---|
| V1.0 | 2018.02.12 |
前言
我們做APP,文字和圖片是絕對(duì)不可缺少的元素,特別是圖片一般存儲(chǔ)在圖床里面,一般公司可以委托第三方保存,NB的公司也可以自己存儲(chǔ)圖片,ios有很多圖片加載的第三方框架,其中最優(yōu)秀的莫過(guò)于SDWebImage,它幾乎可以滿足你所有的需求,用了好幾年這個(gè)框架,今天想總結(jié)一下。感興趣的可以看其他幾篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 圖片類型判斷深入研究
7. SDWebImage探究(七) —— 深入研究圖片下載流程(一)之有關(guān)option的位移枚舉的說(shuō)明
8. SDWebImage探究(八) —— 深入研究圖片下載流程(二)之開(kāi)始下載并返回下載結(jié)果的總的方法
回顧
如果看過(guò)上一篇你就知道,上一篇主要說(shuō)的是調(diào)用接口,然后通過(guò)UIView分類中的一個(gè)方法- (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;返回給調(diào)用接口的completedBlock回調(diào),獲取到你想要的圖片。
那么本篇我們就看一下上面方法中的那個(gè)下載方法。
下載方法內(nèi)部
在上面的方法中,我們調(diào)用了SDWebImageManager的對(duì)象進(jìn)行了下載,返回了id類型遵循協(xié)議的operation對(duì)象,id <SDWebImageOperation> operation
下面我們就看一下該方法的API,幫助大家理解
/**
* Downloads the image at the given URL if not present in cache or return the cached version otherwise.
*
* @param url The URL to the image
* @param options A mask to specify options to use for this request
* @param progressBlock A block called while image is downloading
* @note the progress block is executed on a background queue
* @param completedBlock A block called when operation has been completed.
*
* This parameter is required.
*
* This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter.
* In case of error the image parameter is nil and the third parameter may contain an NSError.
*
* The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
* or from the memory cache or from the network.
*
* The fith parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is
* downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
* block is called a last time with the full image and the last parameter set to YES.
*
* The last parameter is the original image URL
*
* @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
*/
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
從上面描述我們就可以看到,該方法的作用就是如果該圖像在內(nèi)存或者硬盤中不存在,那么就根據(jù)指定的url進(jìn)行下載,如果存在就直接給出cache中的圖像,這個(gè)也是SDWebImage的整體原理架構(gòu)。
下載方法分析
下面我們就一起分析一下這個(gè)比較長(zhǎng)的方法。
1. 容錯(cuò)處理
首先還是對(duì)一些參數(shù)進(jìn)行容錯(cuò)處理。
-
completedBlock不能為空,這里用的是斷言NSAssert
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
- 對(duì)url參數(shù)的類型進(jìn)行了判斷容錯(cuò)處理
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
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;
}
這里針url做了兩方面的容錯(cuò)處理
有些人總是錯(cuò)將NSString對(duì)象傳給NSURL對(duì)象,但是xcode也不會(huì)報(bào)錯(cuò),所以這里進(jìn)行了判斷,傳入的url值如果是NSString對(duì)象就轉(zhuǎn)化為NSURL對(duì)象。
如果url是除了NSString和NSURL以外的對(duì)象,那么就直接讓
url = nil。
2. 下載失敗url集合
這里維護(hù)了一個(gè)集合屬性,用于存放下載失敗的url
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
這里首先判斷,要下載的這個(gè)url是否是在下載失敗的集合列表里面,這里加了鎖,保證線程數(shù)據(jù)安全。
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}
下面接著進(jìn)行條件判斷
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;
}
當(dāng)url的長(zhǎng)度為0或者options枚舉值不是SDWebImageRetryFailed和isFailedUrl == YES的情況下,就直接調(diào)用下面方法直接返回給UIView分類那個(gè)sd_internalSetImageWithURL方法,返回給調(diào)用接口的completionBlock回調(diào)。
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
error:(nullable NSError *)error
url:(nullable NSURL *)url {
[self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
}
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
url:(nullable NSURL *)url {
dispatch_main_async_safe(^{
if (operation && !operation.isCancelled && completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
}
3. 操作集合
這里實(shí)例化了類SDWebImageCombinedOperation并且進(jìn)行了弱化,因?yàn)楹竺鎎lock要使用。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
這里維護(hù)了一個(gè)可變數(shù)組,作為放置下載操作的容器。
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;
利用鎖將這個(gè)操作添加到這個(gè)容器中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
4. 生成圖片緩存的key
下面就是生成圖片緩存的key,這個(gè)key有什么用呢?它有兩個(gè)作用,其一是用來(lái)查詢緩存中對(duì)應(yīng)的圖片資源;其二就是如果內(nèi)存和disk中就需要從網(wǎng)絡(luò)上下載,下載后就要進(jìn)行緩存,那就是用來(lái)存儲(chǔ)圖片緩存的key。
NSString *key = [self cacheKeyForURL:url];
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
下面我們就一起看一下生成的規(guī)則,如果url為nil那么直接就返回空的字符串,否則就判斷這個(gè)blockcacheKeyFilter是否為空,如果不為空就直接調(diào)用block,如果為空就直接返回url完整的表示形式url.absoluteString。下面我們就詳細(xì)的看一下這個(gè)block,先看一下API幫助理解。
typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);
/**
* The cache filter is a block used each time SDWebImageManager need to convert an URL into a cache key. This can
* be used to remove dynamic part of an image URL.
*
* The following example sets a filter in the application delegate that will remove any query-string from the
* URL before to use it as a cache key:
*
* @code
[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
}];
* @endcode
*/
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
補(bǔ)充說(shuō)明
這里舉例和大家說(shuō)明一個(gè)URL的scheme、host等參數(shù)都是什么。
// 以這個(gè)地址為例 https://www.baidu.com/abcdef?a=1
NSString *str = @"https://www.baidu.com/abcdef?a=1";
NSURL *url = [NSURL URLWithString:str];
NSLog(@"url.absoluteString = %@", url.absoluteString);
NSLog(@"url.scheme = %@", url.scheme);
NSLog(@"url.host = %@", url.host);
NSLog(@"url.path = %@", url.path);
下面看一下輸出
2018-02-12 11:04:00.988966+0800 JJWebImage[4751:1325852] url.absoluteString = https://www.baidu.com/abcdef?a=1
2018-02-12 11:04:00.989054+0800 JJWebImage[4751:1325852] url.scheme = https
2018-02-12 11:04:00.989090+0800 JJWebImage[4751:1325852] url.host = www.baidu.com
2018-02-12 11:04:00.989168+0800 JJWebImage[4751:1325852] url.path = /abcdef
這里就不給大家解釋了,看輸出一目了然。
下面我們繼續(xù),通過(guò)上面的步驟我們就生成了圖片緩存的key了。
5. 在SDImageCache中查詢緩存
這里利用SDImageCache類中的下面方法進(jìn)行緩存的查詢。
// typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
/**
* Operation that queries the cache asynchronously and call the completion when done.
*
* @param key The unique key used to store the wanted image
* @param doneBlock The completion block. Will not get called if the operation is cancelled
*
* @return a NSOperation instance containing the cache op
*/
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
這里我們可以看見(jiàn),就是利用上面生成的key進(jìn)行緩存的本地查詢,這個(gè)查詢是異步的,查詢完畢會(huì)返回一個(gè)包含三個(gè)參數(shù)的block - SDCacheQueryCompletedBlock,并且如果該操作被標(biāo)記為取消狀態(tài),那么就不會(huì)調(diào)用這個(gè)doneBlock了。
下面看一下該方法實(shí)現(xiàn)的API
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
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;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
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);
});
}
}
});
return operation;
}
下面就一起分析下這個(gè)查詢方法的實(shí)現(xiàn)。
容錯(cuò)處理
和別的方法一樣,上來(lái)直接就是榮錯(cuò)處理,如果key為nil,那么直接return,返回不帶任何圖像信息的block:doneBlock(nil, nil, SDImageCacheTypeNone);。
檢查內(nèi)存中圖像以及disk中的NSData圖像數(shù)據(jù)
這里首先檢查內(nèi)存中是否包含該圖像,實(shí)現(xiàn)如下:
// First check the in-memory cache...
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;
}
這里利用方法imageFromMemoryCacheForKey:,根據(jù)傳入的key查詢是否有該圖像,接著就是if判斷,針對(duì)圖像類型,如果圖像是GIF類型的需要調(diào)用方法diskImageDataBySearchingAllPathsForKey:進(jìn)行特殊處理,然后調(diào)用block回調(diào)doneBlock(image, diskData, SDImageCacheTypeMemory);。
那么這里就有幾個(gè)問(wèn)題了,第一個(gè)是如何利用方法imageFromMemoryCacheForKey:查詢到對(duì)應(yīng)的image的呢,下面我們就看一下代碼。
//實(shí)例化的時(shí)候初始化內(nèi)存緩存
_memCache = [[AutoPurgeCache alloc] init];
@property (strong, nonatomic, nonnull) NSCache *memCache;
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
這里直接給出了根據(jù)key直接取值的代碼,有取那么就有存儲(chǔ),如下:
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
這里利用類SDImageCacheConfig中的屬性判斷是否要進(jìn)行內(nèi)存緩存
/**
* use memory cache [defaults to YES]
*/
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
如果是要進(jìn)行內(nèi)存緩存,就直接利用memCache進(jìn)行緩存。這樣第1個(gè)問(wèn)題就解決了。
下面就是第二個(gè)問(wèn)題,利用UIImage分類判斷了是否是GIF圖,self.images != nil,如果是GIF圖調(diào)用了方法diskImageDataBySearchingAllPathsForKey:獲取了圖像的NSData數(shù)據(jù)類型,看一下該方法里面的實(shí)現(xiàn)。
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
if (data) {
return data;
}
NSArray<NSString *> *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
if (imageData) {
return imageData;
}
}
return nil;
}
這里,首先要做的是,根據(jù)默認(rèn)路徑獲取NSData類型數(shù)據(jù)。
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
NSString *filename = [self cachedFileNameForKey:key];
return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
const char *str = key.UTF8String;
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];
return filename;
}
可以看見(jiàn),這里是以key為基礎(chǔ),進(jìn)行了MD5加密作為了存儲(chǔ)圖像的文件名,利用NSData的dataWithContentsOfFile:方法獲取了NSData數(shù)據(jù)。如果數(shù)據(jù)不為空,直接就返回了data。
接著,由于https://github.com/rs/SDWebImage/pull/976給磁盤文件名添加了擴(kuò)展,所以這里要特殊處理一下。
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
if (data) {
return data;
}
下面就是維護(hù)一個(gè)可變數(shù)組作為cach的路徑集合,并利用addReadOnlyCachePath:添加了路徑。
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
if (!self.customPaths) {
self.customPaths = [NSMutableArray new];
}
if (![self.customPaths containsObject:path]) {
[self.customPaths addObject:path];
}
}
然后對(duì)數(shù)組進(jìn)行了遍歷,找到path,在根據(jù)key生成文件目錄,在利用NSData的方法生成該類型的數(shù)據(jù)。
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
// fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
// checking the key with and without the extension
imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
if (imageData) {
return imageData;
}
}
這里一樣有文件名擴(kuò)展的問(wèn)題,上面已經(jīng)進(jìn)行了特殊處理。到此為止我們都沒(méi)有進(jìn)行任何下載的操作,所以上面凡是有return的地方都是返回的nil。
實(shí)例化operation
到這里就示例話一個(gè)新的對(duì)象,并且在一個(gè)新的隊(duì)列中執(zhí)行異步操作。
#define SDDispatchQueueSetterSementics strong
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
這里SDDispatchQueueSetterSementics沒(méi)見(jiàn)過(guò),其實(shí)就是宏定義strong而已。
接著就是做了下面這么處理
@autoreleasepool {
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);
});
}
}
這里就是在disk里面根據(jù)key查找了圖像,如果能查找到,并且設(shè)置self.config.shouldCacheImagesInMemory == YES,那么就會(huì)將圖像緩存的內(nèi)存。
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
這些都執(zhí)行完畢后,在主線程里面返回doneBlock這個(gè)block,并return operation;。
到此為止,有關(guān)查找緩存操作- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock的就結(jié)束了,下一篇我們就一起看一下,這個(gè)方法的回調(diào)中的處理。
后記
本篇已結(jié)束,后面更精彩~~~~
