一、流程架构图
SDWebImage对UIButton,UIImageView,NSButton,UIView进行了拓展,并对外提供了接口。无论对UIButton,UIImageView还是NSButton调用sd_setImageWithURL
的时候,最终都会调用到UIView拓展类的sd_internalSetImageWithURL
方法。
前面的拓展都只是对外的接口,主要逻辑处理放在SDWebImageManager
里面。他相当于一个调度中心
,如果需要缓存(读跟取),他就会调用SDImageCache
,如果需要下载,就会调用SDWebImageDownloader
。类似我们MVP模式下的Presenter
,收到View拓展接口相关的参数后,根据不同业务传递给cache跟downloader处理,最后将处理完的数据通过block回调给接口。
最后还有一些工具,没有在流程图中画出来,这里说明一下:
Decoder
:做一些编解码操作,针对不同类型的图片进行不同的操作。
Transform
:从缓存或下载转换图像加载的转换器协议。
AnimatedImage
:可以替代UIImageView,支持gif
Utils
:存放一些枚举,Define,还有菊花器
Categories
:对需要的类进行拓展,大部分是UIImage
Private
:一些私人方法
二、代码部分 1. UIView+WebCache
直接找到sd_internalSetImageWithURL
方法,这是入口进来后第一个处理的方法,处理内容如下:
a. 拿到旧的operation(任务),取消其操作,并从SDOperationsDictionary
移除。然后创建新的加载任务,并加入到SDOperationsDictionary
中。
b. 处理进度条,重置进度条
c. 处理菊花器
d. 创建SDWebImageManager,并调用loadImageWithURL
加载图片
我们先看sd_internalSetImageWithURL
方法里面的代码
a、取消之前的任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; if (!validOperationKey) { validOperationKey = NSStringFromClass ([self class ]); SDWebImageMutableContext *mutableContext = [context mutableCopy]; mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey; context = [mutableContext copy ]; } self .sd_latestOperationKey = validOperationKey; [self sd_cancelImageLoadOperationWithKey:validOperationKey]; self .sd_imageURL = url;
这段代码主要是拿到validOperationKey
,并传给sd_cancelImage
方法,sd_cancelImage
的逻辑也很简单,通过validOperationKey
,在SDOperationsDictionary
里面拿到对应的任务,并取消
。下面是sd_cancelImage
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 - (void )sd_cancelImageLoadOperationWithKey:(nullable NSString *)key { if (key) { SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; id <SDWebImageOperation> operation; @synchronized (self ) { operation = [operationDictionary objectForKey:key]; } if (operation) { if ([operation conformsToProtocol:@protocol (SDWebImageOperation )]) { [operation cancel]; } @synchronized (self ) { [operationDictionary removeObjectForKey:key]; } } } }
b、占位图显示 1 2 3 4 5 6 7 if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; }); }
c、进度条,菊花器处理逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 if (url) { NSProgress *imageProgress = objc_getAssociatedObject(self , @selector (sd_imageProgress)); if (imageProgress) { imageProgress.totalUnitCount = 0 ; imageProgress.completedUnitCount = 0 ; } #if SD_UIKIT || SD_MAC [self sd_startImageIndicator]; id <SDWebImageIndicator> imageIndicator = self .sd_imageIndicator; #endif SDWebImageManager *manager = context[SDWebImageContextCustomManager]; if (!manager) { manager = [SDWebImageManager sharedManager]; } else { SDWebImageMutableContext *mutableContext = [context mutableCopy]; mutableContext[SDWebImageContextCustomManager] = nil ; context = [mutableContext copy ]; } SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { if (imageProgress) { imageProgress.totalUnitCount = expectedSize; imageProgress.completedUnitCount = receivedSize; } #if SD_UIKIT || SD_MAC if ([imageIndicator respondsToSelector:@selector (updateIndicatorProgress:)]) { double progress = 0 ; if (expectedSize != 0 ) { progress = (double )receivedSize / expectedSize; } progress = MAX(MIN(progress, 1 ), 0 ); dispatch_async (dispatch_get_main_queue(), ^{ [imageIndicator updateIndicatorProgress:progress]; }); } #endif if (progressBlock) { progressBlock(receivedSize, expectedSize, targetURL); } };
注意这里并没有直接调用combinedProgressBlock
处理进度条,而是在下面加载图片的时候将combinedProgressBlock
扔过去处理。
d、通过SDWebImageManager调用加载图片的方法 这里调用了SDWebImageManager
的图片加载方法,将一些必要参数传递过去,接下来就是SDWebImageManager
的事情了。
1 [ manager loadImageWithURL: url options: options context: context progress: combinedProgressBlock completed: ^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
2. SDWebImageManager
直接找到loadImageWithURL
方法,这个方法主要是对url
的一些判断,context
与options
的预处理,内容如下:
a. 先判断url的可行性
b. 对context
,options
进行预处理,并放到result
里面
c. 调用callCacheProcessForOperation
判断是否有缓存,如果有则进入ImageCache
拿到缓存数据,如果没有则进入callDownloadProcessForOperation
方法进一步判断如何下载
先看看这些步骤的源码,看完再看callCacheProcessForOperation
做了些什么
a、判断url的可行性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 NSAssert (completedBlock != nil , @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead" ); if ([url isKindOfClass:NSString .class]) { url = [NSURL URLWithString:(NSString *)url]; } if (![url isKindOfClass:NSURL .class]) { url = nil ; } SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; operation.manager = self ; BOOL isFailedUrl = NO ; if (url) { SD_LOCK(_failedURLsLock); isFailedUrl = [self .failedURLs containsObject:url]; SD_UNLOCK(_failedURLsLock); } if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil" ; NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL; [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url]; return operation; }
这里注释应该很清楚了,就是判断url的可行性,跟url是否在失败列表里面,如果在的,且options
没有SDWebImageRetryFailed
的话,就直接失败回调。值得注意的是SD的锁在iOS10以上用的是os_unfair_lock
,iOS10以下用的是OSSpinLockLock
(这个锁存在任务优先级问题,已经被淘汰了)
b、对context
,options
进行预处理,并放到result
里面 1 2 3 4 5 6 7 8 9 10 11 SD_LOCK(_runningOperationsLock); [self .runningOperations addObject:operation]; SD_UNLOCK(_runningOperationsLock); SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
这里先将任务加入到正在执行的列表里面,然后再对context
进行预处理,源代码是没有对options
进行说明处理的,然后将context
跟options
放入result里面。context的处理源代码就不贴出来了,大概就是对SDWebImageContextImageTransformer
、SDWebImageContextCacheKeyFilter
、SDWebImageContextCacheSerializer
这3个进行一个判断,看是否有自定义的传过来,没有就用默认的。
c、callCacheProcessForOperation
的调用 这里主要是判断要到哪里去取数据,ImageCache,还是去下载,接下来就进入这个方法看一下。
这里主要是判断任务是否该走缓存查询,或者直接下载。如果是缓存查询,就进入SDImageCache
里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation
进入下一步判断。
①. 拿到imageCache
,拿到缓存类型queryCacheType
②. 通过 options
判断,走缓存还是下载。如果走缓存,则调用SDImageCache
里面的queryImageForKey
(开始进入SDImageCache
的逻辑);如果走下载,则调用callDownloadProcessForOperation
开始下载前的一些处理。
①、拿到imageCache
,拿到缓存类型queryCacheType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 id <SDImageCache> imageCache; if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol (SDImageCache )]) { imageCache = context[SDWebImageContextImageCache]; } else { imageCache = self .imageCache; } SDImageCacheType queryCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextQueryCacheType]) { queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue]; }
②、通过options
,判断缓存查找,还是下载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { NSString *key = [self cacheKeyForURL:url context:context]; @weakify(operation); operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache" }] url:url]; [self safelyRemoveOperationFromRunning:operation]; return ; } else if (context[SDWebImageContextImageTransformer] && !cachedImage) { [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock]; return ; } [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock]; }]; } else { [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; }
这里解释一下key
是怎么拿(SDWebImage的缓存key是怎么样的),逻辑在这个方法里面cacheKeyForURL
,代码就不贴出来了,说一下大概逻辑。
a、SDWebImage
的context
里面有个SDWebImageContextCacheKeyFilter
,里面存储的是用来存放自定义key
逻辑的协议
,通过重写cacheKeyForURL
自定义key,如果没有传SDWebImageContextCacheKeyFilter
进来则使用url的string
值。 b、然后通过context
里面的SDWebImageContextImageThumbnailPixelSize
、SDWebImageContextImagePreserveAspectRatio
和SDWebImageContextImageTransformer
这3个里面是否有值,如果有值就加上上面的key
进行拼接,没值就直接用上面的key
。
查到缓存后就是回调了,回调看代码注释,问题应该不大,要注意的是它也走了callDownloadProcessForOperation
这个方法,因为options
为SDWebImageRefreshCached
的情况下,也是要走下载的,所以索性将找到的缓存,放到callDownloadProcessForOperation
处理,而不是直接回调。
3.SDImageCache
缓存获取数据,主要是通过key
缓存,cacheType
判断缓存方式,options
进行缓存拓展。主要内容如下:
a. 对cacheOptions
类型进行筛选
b. 进入queryCacheOperationForKey
方法,对具体缓存方式进行划分,其中包括内存缓存,磁盘缓存。然后又在各自缓存下面进行了详细划分
a、内存查找 为啥说缓存的查找是先内存呢,看下面这段代码:
1 2 3 4 5 6 UIImage *image; if (queryCacheType != SDImageCacheTypeDisk) { image = [self imageFromMemoryCacheForKey:key]; }
一般的queryCacheType
默认为SDImageCacheTypeAll
,在没有自定义queryCacheType
为SDImageCacheTypeDisk
的情况下都是先走的memoryCache
而imageFromMemoryCacheForKey
这个方法里面的查找方式也很简单,通过封装SDMemoryCache
协议,并用NSMapTable<KeyType, ObjectType>
类型存储的值去取到对应的image
1 2 3 4 5 6 7 8 9 BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil , SDImageCacheTypeMemory); } return nil ; }
正常情况下,如果照片找到了,就直接回调block。但是在queryCacheType
不指定为SDImageCacheTypeMemory
,且options
为SDImageCacheQueryMemoryData
的时候那就得继续往下,去磁盘
查找。
b、磁盘查找 磁盘查找分为同步跟异步,默认情况是异步查找,以下情况是同步查找
1 2 3 4 5 6 7 BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) || (!image && options & SDImageCacheQueryDiskDataSync));
磁盘的查找有2种方式
一种是通过SDDiskCache
协议内部封装的方法,通过key获取path,然后拿到data 一种是通过additionalCachePathBlock
拿到保存的path,然后拿到data 如果是通过磁盘拿到的image,还会将image保存到内存,以便下次查询。
这里说个小细节,磁盘查询的过程是用了@autoreleasepool
包了起来,为了防止多张照片查询,引起的内存飙升。