版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.02.12 |
前言
我们做APP,文字和图片是绝对不可缺少的元素,特别是图片一般存储在图床里面,一般公司可以委托第三方保存,NB的公司也可以自己存储图片,ios有很多图片加载的第三方框架,其中最优秀的莫过于SDWebImage,它几乎可以满足你所有的需求,用了好几年这个框架,今天想总结一下。感兴趣的可以看其他几篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 图片类型判断深入研究
7. SDWebImage探究(七) —— 深入研究图片下载流程(一)之有关option的位移枚举的说明
8. SDWebImage探究(八) —— 深入研究图片下载流程(二)之开始下载并返回下载结果的总的方法
回顾
如果看过上一篇你就知道,上一篇主要说的是调用接口,然后通过UIView分类中的一个方法- (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;
返回给调用接口的completedBlock
回调,获取到你想要的图片。
那么本篇我们就看一下上面方法中的那个下载方法。
下载方法内部
在上面的方法中,我们调用了SDWebImageManager
的对象进行了下载,返回了id类型遵循协议的operation对象,id
下面我们就看一下该方法的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 )loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
从上面描述我们就可以看到,该方法的作用就是如果该图像在内存或者硬盘中不存在,那么就根据指定的url进行下载,如果存在就直接给出cache中的图像,这个也是SDWebImage
的整体原理架构。
下载方法分析
下面我们就一起分析一下这个比较长的方法。
1. 容错处理
首先还是对一些参数进行容错处理。
-
completedBlock
不能为空,这里用的是断言NSAssert
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
- 对url参数的类型进行了判断容错处理
// 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做了两方面的容错处理
有些人总是错将NSString对象传给NSURL对象,但是xcode也不会报错,所以这里进行了判断,传入的url值如果是NSString对象就转化为NSURL对象。
如果url是除了NSString和NSURL以外的对象,那么就直接让
url = nil
。
2. 下载失败url集合
这里维护了一个集合属性,用于存放下载失败的url
@property (strong, nonatomic, nonnull) NSMutableSet *failedURLs;
这里首先判断,要下载的这个url是否是在下载失败的集合列表里面,这里加了锁,保证线程数据安全。
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject: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;
}
当url的长度为0或者options枚举值不是SDWebImageRetryFailed
和isFailedUrl == YES
的情况下,就直接调用下面方法直接返回给UIView分类那个sd_internalSetImageWithURL
方法,返回给调用接口的completionBlock
回调。
- (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. 操作集合
这里实例化了类SDWebImageCombinedOperation
并且进行了弱化,因为后面block要使用。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
这里维护了一个可变数组,作为放置下载操作的容器。
@property (strong, nonatomic, nonnull) NSMutableArray *runningOperations;
利用锁将这个操作添加到这个容器中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
4. 生成图片缓存的key
下面就是生成图片缓存的key,这个key有什么用呢?它有两个作用,其一是用来查询缓存中对应的图片资源;其二就是如果内存和disk中就需要从网络上下载,下载后就要进行缓存,那就是用来存储图片缓存的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;
}
}
下面我们就一起看一下生成的规则,如果url为nil那么直接就返回空的字符串,否则就判断这个blockcacheKeyFilter
是否为空,如果不为空就直接调用block,如果为空就直接返回url完整的表示形式url.absoluteString
。下面我们就详细的看一下这个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;
补充说明
这里举例和大家说明一个URL的scheme、host
等参数都是什么。
// 以这个地址为例 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
这里就不给大家解释了,看输出一目了然。
下面我们继续,通过上面的步骤我们就生成了图片缓存的key了。
5. 在SDImageCache中查询缓存
这里利用SDImageCache类中的下面方法进行缓存的查询。
// 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;
这里我们可以看见,就是利用上面生成的key进行缓存的本地查询,这个查询是异步的,查询完毕会返回一个包含三个参数的block - SDCacheQueryCompletedBlock
,并且如果该操作被标记为取消状态,那么就不会调用这个doneBlock
了。
下面看一下该方法实现的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;
}
下面就一起分析下这个查询方法的实现。
容错处理
和别的方法一样,上来直接就是荣错处理,如果key为nil,那么直接return,返回不带任何图像信息的block:doneBlock(nil, nil, SDImageCacheTypeNone);
。
检查内存中图像以及disk中的NSData图像数据
这里首先检查内存中是否包含该图像,实现如下:
// 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:
,根据传入的key查询是否有该图像,接着就是if判断,针对图像类型,如果图像是GIF类型的需要调用方法diskImageDataBySearchingAllPathsForKey:
进行特殊处理,然后调用block回调doneBlock(image, diskData, SDImageCacheTypeMemory);
。
那么这里就有几个问题了,第一个是如何利用方法imageFromMemoryCacheForKey:
查询到对应的image的呢,下面我们就看一下代码。
//实例化的时候初始化内存缓存
_memCache = [[AutoPurgeCache alloc] init];
@property (strong, nonatomic, nonnull) NSCache *memCache;
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memCache objectForKey:key];
}
这里直接给出了根据key直接取值的代码,有取那么就有存储,如下:
// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
这里利用类SDImageCacheConfig
中的属性判断是否要进行内存缓存
/**
* use memory cache [defaults to YES]
*/
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
如果是要进行内存缓存,就直接利用memCache
进行缓存。这样第1个问题就解决了。
下面就是第二个问题,利用UIImage分类判断了是否是GIF图,self.images != nil
,如果是GIF图调用了方法diskImageDataBySearchingAllPathsForKey:
获取了图像的NSData数据类型,看一下该方法里面的实现。
- (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 *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;
}
这里,首先要做的是,根据默认路径获取NSData类型数据。
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;
}
可以看见,这里是以key为基础,进行了MD5加密作为了存储图像的文件名,利用NSData
的dataWithContentsOfFile:
方法获取了NSData数据。如果数据不为空,直接就返回了data。
接着,由于https://github.com/rs/SDWebImage/pull/976
给磁盘文件名添加了扩展,所以这里要特殊处理一下。
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
if (data) {
return data;
}
下面就是维护一个可变数组作为cach的路径集合,并利用addReadOnlyCachePath:
添加了路径。
@property (strong, nonatomic, nullable) NSMutableArray *customPaths;
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
if (!self.customPaths) {
self.customPaths = [NSMutableArray new];
}
if (![self.customPaths containsObject:path]) {
[self.customPaths addObject:path];
}
}
然后对数组进行了遍历,找到path,在根据key生成文件目录,在利用NSData的方法生成该类型的数据。
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。
实例化operation
到这里就示例话一个新的对象,并且在一个新的队列中执行异步操作。
#define SDDispatchQueueSetterSementics strong
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
这里SDDispatchQueueSetterSementics
没见过,其实就是宏定义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里面根据key查找了图像,如果能查找到,并且设置self.config.shouldCacheImagesInMemory == YES
,那么就会将图像缓存的内存。
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
这些都执行完毕后,在主线程里面返回doneBlock这个block,并return operation;
。
到此为止,有关查找缓存操作- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock
的就结束了,下一篇我们就一起看一下,这个方法的回调中的处理。
后记
本篇已结束,后面更精彩~~~~