SDWebImage 分析

SDWebImage 分析

Version 4.0.0


导航

按照模块分析 SDWebImage

1. UI交互的基类 UIView+WebCache

2. SDWebImage 的主要管理者 SDWebImageManager

3. 缓存模块 SDImageCache

4. 下载模块 SDWebImageDownloader

5. 下载的执行者 SDWebImageDownloaderOperation

6. 预加载 SDWebImagePrefetcher

7. GIF子模块 FLAnimatedImage


UIKit 交互1 -- UIView+WebCache

1. 接口定义

a. 下载图片

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

b. 下载已经缓存过的图片

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock;

c. 下载动图

- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray *)arrayOfURLs;

d. 取消下载动图

- (void)sd_cancelCurrentAnimationImagesLoad;

2. 分析

a. 下载图片

下载图片有数个方法定义, 见UIView+WebCache.h, 最终都调用了UIView+WebCache中的 sd_internalSetImageWithURL 这个方法.

sd_internalSetImageWithURL 方法做的事情:

  • 首先取消了当前 View 所绑定的一切请求.
  • 设置placeholder.
  • 调用SDWebImageManager下载图片, 并将方法返回的 operation 与当前 View 绑定.
  • 下载图片回调处理.

Tips
dispatch_main_async_safe 定义了在主线程进行UI操作的宏:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

weakSelf 为 nil 时候直接结束避免崩溃或者其他错误.

b. 下载已经缓存过的图片

  • 首先调用SDImageCache从缓存中读取Image.
  • 再直接执行上一步下载图片的代码.
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];

[self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];

Tips : 在执行下载的过程中, 如果找到了缓存, 就忽略placeholder, 避免一次无效操作.

c. 下载动图

  • 取消当前View 绑定的动图下载操作.
  • 遍历传入的URL数组, 对每个URL调用SDWebImageManager下载图片, 并将方法返回的 operation 装入数组,再将这个数组与当前 View 绑定.

PS. 这也解释了 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key 为什么会有如下看起来很奇怪的代码, operation的类型并不是固定的.

SDOperationsDictionary *operationDictionary = [self operationDictionary];
id operations = operationDictionary[key];
if (operations) {
    if ([operations isKindOfClass:[NSArray class]]) {
        for (id  operation in operations) {
            if (operation) {
                [operation cancel];
            }
        }
    } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
        [(id) operations cancel];
    }
    [operationDictionary removeObjectForKey:key];
}

Tips : [self operationDictionary] 使用 Runtime 为实例增加了变量.

Question : 如何保证下载的顺序?

d. 取消下载动图

  • 直接调用取消方法 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key

Tips : 关于id operation解释:
每个 operaton 都有实现一个 - (void)cancel; 方法, 这个是在SDWebImageOperation协议中定义, 无论是什么类型实例, 只要实现了该协议, 都可以统一调用,详细解释可以搜索iOS+面向接口编程.

3. 小结

UIView+WebCache 模块中, 只做了一些简单的操作, 定义好了与 UIKit 交互接口, 下载与取消交给了 SDWebImageManager 处理, 缓存交给了 SDImageCache 处理.


SDWebImage幕后管理者 -- SDWebImageManager

1. 接口定义

a. 缓存模块

@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;

b. 下载模块

@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

c. 下载图片

- (nullable id )loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

d. 手动设置图片缓存

- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;

e. 取消所有的操作

- (void)cancelAll;

f. 当前是否有操作在运行

- (BOOL)isRunning;

g. 异步检查图片是否已经被缓存

- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

h. 异步检查图片是否已经被缓存在了磁盘上

- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

i. 获取URL缓存索引的Key

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

2. 分析

a. 缓存模块

  • 通过 [SDImageCache sharedImageCache] 引用到了 SDImageCache 的单例.

b. 下载模块

  • 通过 [SDWebImageDownloader sharedImageCache] 引用到了 SDWebImageDownloader 的单例.

c. 下载图片

  • 判断输入错误处理等, 并生成一个 SDWebImageCombinedOperation 实例, 也就是上文提到过的一个operation, 然后将改operation加入self.runningOperations方便管理;
  • 使用 [SDImageCache queryCacheOperationForKey: done:] 生成了一个NSOperation实例 并赋值给了上一步所使用的operation的cacheOperation属性, 方便执行cancel方法.
  • 上一步中, 在 SDImageCache 中先在内存中找图片的缓存,找到直接执行回调,若没找到则在硬盘上找缓存,若找到切可以在内存做缓存,则在内存中做缓存, 然后执行回调.
  • queryCacheOperationForKey方法的回调中,如果发现当前Operation被取消了,则将该operation从self.runningOperations移除,并使用@synchronized保证线程安全.
  • 如果在缓存找到了图片,执行回调,并移除operation
  • 如果没有找到, 则调用[SDWebImageDownloader downloadImageWithURL:options:progress:completed:]方法下载图片, 并将该方法换回的token标志绑定到operation的cancelBlock中, 方便取消下载请求;
  • 下载完成之后之后, 执行imageManager:transformDownloadedImage:withURL:方便使用者在下载完成时候立刻对图片做一些自定义处理,再将该图片进行缓存. 最后执行回调并移除改operation.

d. 手动设置图片缓存

  • 直接调用[SDImageCache storeImage:forKey:toDisk:completion:]

e. 取消所有的操作

  • 因为涉及到 self.runningOperations 的读写, 因此用 @synchronized 保证线程安全.
  • copy 了一份self.runningOperations, 并使用 [NSArray makeObjectsPerformSelector:] 方法取消队列中所有的操作.
  • 将复制的队列中的所有元素从self.runningOperations移除.

f. 当前是否有操作在运行

  • 为保证原子性,使用@synchronized访问self.runningOperations

g. 异步检查图片是否已经被缓存

  • 分别调用[SDImageCache imageFromMemoryCacheForKey:][SDImageCache diskImageExistsWithKey:completion:] 方法检查是否在内存和硬盘上缓存.
  • 由于检查硬盘是否缓存要用到专门的IO线程(在SDImageCache中定义), 调用者不可能去等待IO线程,因此此方法被设计为异步方法.

h. 异步检查图片是否已经被缓存在了磁盘上

  • 调用[SDImageCache diskImageExistsWithKey:completion:] 方法检查是是否在硬盘上缓存.

i. 获取URL缓存索引的Key

  • 如果定义了self.cacheKeyFilter自定义存储Key,则使用该回调获取用于缓存索引的Key

3. 小结

SDWebImage可以看出作者考虑到了很多一般开发者不会去考虑的事情, 简单的如线程安全, 更细致的如imageManager:transformDownloadedImage:withURL:方法, 方便使用SDWebImage的人在使用之前先处理, 再缓存, 一个个简单的应用场景是用户想对一张网络图片进行模糊处理, 一般的步骤是先用SDWebImage下载,然后自行模糊处理,再展示. 但如果有大量图片要处理, 又涉及到tableView的复用问题, 为了提高性能, 使用者要自己对模糊之后的图片做缓存, 优化缓存策略和IO潜在地问题等等. 实际上SDWebImage 已经可以处理这个问题而不需要使用者再去考虑.


SDWebImage缓存模块 -- SDImageCache

1. 接口定义

a. 缓存图片

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

b. 缓存图片到硬盘上(只能从IO Queue 调用)

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

c. 检查图片是否在硬盘上缓存(只检查, 不会把图片加到内存)

- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

d. 检查是否有缓存

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

e. 从内存中取缓存的图片(同步)

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

f. 从硬盘取缓存的图片(同步)

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

g. 从缓存中取图片(同步)

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

h. 从内存和硬盘删除图片缓存(异步)

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

i. 从内存移除缓存, 选择是否从硬盘移除

- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

j. 清除内存缓存

- (void)clearMemory;

k. 异步清除磁盘缓存

- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

l. 异步清除硬盘上已经过期的缓存

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

2. 分析

初始化方法

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory;

初始化各个属性:

@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

生成的一个串行队列,专用于IO操作. 不再使用的时候应该使用dispatch_release释放队列.

@property (strong, nonatomic, nonnull) NSCache *memCache;

NSCache是iOS系统提供的缓存类,通过键值对对需要缓存的对象作强引用来达到缓存的目的.

NSFileManager *_fileManager;
dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

注意,生成_fileManager在ioQueue中,并且是是一个同步操作, 之后_fileManager都要在ioQueue中进行.

a. 缓存图片

  • 首先根据SDImageCacheConfig判断是否在在内存中缓存,使用[NSCache setObject:forKey:cost:]方法缓存.
  • 根据toDisk参数判断是否在磁盘中缓存,在ioQueue中调用[self storeImageDataToDisk:data forKey:key];缓存到硬盘,使用@autoreleasepool释放临时变量.
  • 最后在主线程执行回调.

Tips
NSCache 有最大缓存容积的设置totalCostLimit, 但是这个设置只有在设置缓存的时候指定要缓存对象占用的字节数(cost)才能生效. 但是对象的内存占用计算十分复杂, SDWebImage只是给出了一个大致值image.size.height * image.size.width * image.scale * image.scale;.

b. 缓存图片到硬盘上(只能从IO Queue 调用)

此方法只能在ioQueue中调用,奇怪的是SDImageCache并没有暴露ioQueue访问, 因此, 将此方法暴露在.h文件是没有意义的.

  • 首先检查是不是在ioQueue
  • 使用key的16位MD5编码+文件后缀作为缓存文件名生成存储路径, 如果目标路径不存在,则创建该路径
  • 使用[NSfileManager createFileAtPath:contents:attributes:]将下载下来的图片的原始二进制数据写磁盘

c. 检查图片是否在硬盘上缓存(只检查, 不会把图片加到内存)

  • 先后用key参数的md5编码组合路径找是都存在文件,在用key参数的md5编码+文件后缀寻找是否存在
  • 在主线程中回调

在某个版本之前, 硬盘缓存没有文件后缀名, 为了兼容, 要做两次查找

d. 检查是否有缓存

  • 先调用imageFromMemoryCacheForKey, 并用结果执行回调, 注意这一步是同步操作, 因此不需要 NSOperation 来取消操作, 故返回nil.
  • 再调用diskImageDataBySearchingAllPathsForKey在硬盘找缓存, 如果找到并且条件允许, 在内存缓存该图片. 这一步要在ioQueue中异步执行, 可以利用 NSOperation 取消这一步操作. 因此在执行回调后返回该NSOperation.

e. 从内存中取缓存的图片(同步)

  • 直接在self.memCache读取, 可能情况是没有缓存-返回nil, 有缓存但是缓存已经被释放-返回nil, 或是寻找缓存命中-返回目标图片.

f. 从硬盘取缓存的图片(同步)

  • 在硬盘上找目标二进制文件,找到后调用UIImage *image = [UIImage sd_imageWithData:data];image = [self scaledImageForKey:key image:image];生成目标图片,若允许,在内存中缓存该图片.

理论上来说, 这句话放在ioQueue中执行会好一些, 猜测可能是需要同步执行

g. 从缓存中取图片(同步)

  • 直接调用上面两个方法.

h. 从内存和硬盘删除图片缓存(异步)

  • 直接调用下方的方法.

i. 从内存移除缓存, 选择是否从硬盘移除

  • 直接从内存缓存中移除, (NSCache是线程安全的)
  • 在ioQueue中移除目标文件, 并在主线程回调

j. 清除内存缓存

  • 直接移除self.memCache所有对象

k. 异步清除磁盘缓存

  • 调用下方方法

l. 异步清除硬盘上已经过期的缓存

  • 遍历缓存目录下每一个文件, 获取其属性, 若获取失败或是该文件是文件夹则跳过
  • 若文件的修改时间早于设定时间,则将文件地址加入待删除列表.
  • 遍历结束, 从硬盘移除待删除列表中的文件.
  • 计算未删除的文件的总大小, 若仍大于目标大小, 进一步删除最早改动的文件.

Tips : [[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]这个比较挺有意思.

3. 小结

这一个模块中,有内存与硬盘两级缓存, NSCache 在系统级别保证了线程安全,相对来说处理容易. 但是IO操作本身较为耗时, 单独创建一个队列作为ioQueue来进行IO操作, 达到在硬盘上缓存的目的.

SDWebImage下载模块 -- SDWebImageDownloader

1. 接口定义

a. 初始化

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;

b. 设置请求的Header

- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;

c. 下载图片

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

d. 取消下载

- (void)cancel:(nullable SDWebImageDownloadToken *)token;

e. 暂停下载

- (void)setSuspended:(BOOL)suspended;

f. 取消所有的下载

- (void)cancelAllDownloads;

2. 分析

a. 初始化

  • _downloadQueue 是一个NSOperationQueue实例, 每个URL的请依赖这个queue进行管理.
  • _URLOperations 用于存储所有的operation实例, 每个URL对应一个operation.
  • _barrierQueue 是一个并行队列, operation的创建于取消都在这个队列中完成.
  • self.session 是用于网络请求的NSURLSession组件, 所有的operation对这个session保持了弱引用.

b. 设置请求的Header

c. 下载图片

  • 直接调用并返回了[SDWebImageDownloader addProgressCallback:completedBlock:forURL:createCallback:]方法, 以下分析addProgressCallback.
  • 首先使用dispatch_barrier_sync方法, 这是一个同步方法, 但是参数self.barrierQueue是一个并发队列, 因此当前线程会等待bolck中执行完(由于使用的是dispatch_barrier_sync, 而不是dispatch_sync,所以当前block也会等待self.barrierQueue中已经添加的任务执行完).
  • 如果 执行addProgressCallback最后一个参数createCallback(), 并返回一个operation, 注意,执行createCallback又回到了上一层方法downloadImageWithURL方法中.
  • downloadImageWithURL方法, 首先组装好了一个request, 然后生成了一个SDWebImageDownloaderOperation或者其子类的的实例, 并将这个operaion加入了self.downloadQueue队列中. 如果这个队列严格采用LIFO(是栈不是队), 那么上一个加入的operation要依赖于这个operation, 用[sself.lastAddedOperation addDependency:operation];达成目的. 最后返回这个operation. 然后又回到了addProgressCallback这个方法. 吐下槽, 思路有点纠结.
  • 回到addProgressCallback方法后,执行[operation addHandlersForProgress:progressBlock completed:completedBlock]将两个Block绑定到operation中, 复制使用的[NSBlock copy]方法, 避免不必要的引用. 注意这儿同一个url可能被请求多次, 因此一个url绑定一个operation, 一个operation绑定多个执行回调
  • 返回cancelToken 下载方法执行结束.

Tips : 怎么开始下载的? SDWebImageDownloaderOperation继承了NSOperation, 并重写了start()方法, 并在start()方法中调用了[self.dataTask resume];开始下载.

d. 取消下载

  • 放在 self.barrierQueue 异步执行.
  • 执行[SDWebImageDownloaderOperation cancel:]方法.

Tips : [SDWebImageDownloaderOperation cancel:]首先将token对应的callback移除掉. 当所有的callbacl都移除掉之后, 会调用父类NSOperationcancel方法, 这会将isCancelled属性置为YES, 在start方法调用的时候就不会真正执行. 最后调用[self.dataTask cancel];关闭数据传输.

Question: 手动调cancel方法后, 就不会执行失败的block了吗?

e. 暂停下载

  • 直接调用(self.downloadQueue).suspended = suspended;, 这儿利用了NSOperationQueue的功能.

f. 取消所有的下载

  • [self.downloadQueue cancelAllOperations];
  • 自动调用SDWebImageDownloaderOperationcancel方法.

3. 小结

这一个模块开始进行图片下载相关代码的执行, 然而真正的下载代码还是被放在了SDWebImageDownloaderOperation中, 'SDWebImageDownloader'模块的分析只是对SDWebImageDownloaderOperation做了简单的描述, 主要还是重点分析本模块所做的事情--管理所有的下载行为. 此外, self.downloadQueue保证了对self.URLOperations操作能并发, 但又不相互干扰(同时保证异步和并发, 但实际上并没有并发).

SDWebImage下载的执行者 -- SDWebImageDownloaderOperation


1. 接口定义

a. 初始化

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

b. 存储回调Block

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

c. 开始(继承父类)

- (void)start;

d. 取消(继承父类)

- (void)cancel;

e. 是否在执行(继承父类)

- (void)setFinished:(BOOL)finished;

f. 是否已结束(继承父类)

- (void)setExecuting:(BOOL)executing;

g. 取消单个操作

- (BOOL)cancel:(nullable id)token;

2. 分析

a. 初始化

  • 首先将参数中的request复制了一份, 注意, NSURLRequest实现了NSCopying协议.
  • 初始化了存储block回调的数组_callbackBlocks.
  • 将参数session复制给了_unownedSession属性, 注意这儿是弱引用, 避免不必要的引用.
  • 生成了一个并发队列_barrierQueue,用于_callbackBlocks的增删操作,保证线程安全.

b. 存储回调Block

  • 在这儿将progressBlockcompletedBlock两个block都复制了一份,再存储到_callbackBlocks中.
  • 在这儿也使用了dispatch_barrier_async方法, 这是个异步操作

c. 开始(继承父类)

  • 注意, 这儿虽然覆盖了父类的start方法, 但是不能调用[super start];
  • SDWebImage中, Operation被加到SDWebImageDownloaderdownloadQueue中后会被自动执行, (自动调用operationstart方法)
  • 首先判断自己是否被取消了
  • 再判断self.unownedSession是否还在, 一般情况下是还在的, 因为默认的SDWebImageDownloader是个单例不会被释放, 但如果开发者自己初始化一个SDWebImageDownloader就会存在self.unownedSession不再引用一个session的情况.
  • 根据sessionrequest生成一个dataTask, 并将自己标记为正在执行.
  • 开始下载, 并触发第一次'progressBlock'.

d. 取消(继承父类)

  • 若已经完成, 直接返回.
  • 调用[super cancel], 会将isFinished标记为YES.
  • 取消下载操作.

e. 是否在执行(继承父类)

  • 用KVO通知值改变.

f. 是否已结束(继承父类)

  • 同上.

g. 取消单个操作

  • 根据token将对应的回调从``删除, 在这儿使用了[NSArray removeObjectIdenticalTo:]方法, 利用"本体性"而不是"相等性"去移除对应的回调, 个人猜测是为了提高查找的速度. 具体可以参考Equality这篇文章.
  • 在这儿使用了dispatch_barrier_sync, 注意这儿是一个同步方法, 后面根据移除后_callbackBlocks是否为空判断是否要停止当前的下载.

Tips: startcancel@synchronized保证的线程安全, 对_callbackBlocks的操作使用一个队列保障线程安全. 此外, operation持有两个session, 一个是unownedSession, 这个由SDWebImageDownloader持有, operation对它保持弱引用, 还有一个是ownedSession, 当初始化的session被释放时候, 使用自己生成的session, 并用ownedSession保持引用, 并在[self reset]中释放这个session.

3. 小结

这个Operation完成了SDWebImage最重要的下载功能. 将一个URL的下载下载封装成一个NSOperation, 特别是在线程安全上做了一些优化, 和使用异步或是同步, 哪些操作需要保证线程安全, 哪些元素需要复制, 值得思考. 在SDWebImage的issue中有很多于此模块有关的, 值得细看.

SDWebImage 预加载 -- SDWebImagePrefetcher


1. 接口定义

1. 初始化

- (nonnull instancetype)initWithImageManager:(nonnull SDWebImageManager *)manager;

2. 执行预加载

- (void)prefetchURLs:(nullable NSArray *)urls
            progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
           completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;

3. 取消

- (void)cancelPrefetching;

2. 分析

1. 初始化

  • 默认的init方法生成了一个新的SDWebImageManager实例, 在这儿使用了[SDWebImageManager new], 调用的是SDWebImageManager的方法默认初始化方法. 因此, 这儿的manager和[SDWebImageManager sharedManager]不是一个实例, 但是由于SDWebImageManager的默认初始化方法中使用的[SDImageCache sharedImageCache][SDWebImageDownloader sharedDownloader]单例, 所以在这儿初始化的manager和[SDWebImageManager sharedManager]共享的同一个_imageCache_imageDownloader实例.
  • 举个例子, 在我写的一个测试程序中, [SDWebImageManager sharedManager]的内存地址是0x61000107f280, 而SDWebImagePrefetcher所持有的manager地址是0x000060000106ef00, 他们不是同一个manager, 当我打印[SDWebImageManager sharedManager]的各个属性时候, 如下方结果, _imageCache_imageDownloader的地址是一致的.
(lldb) pinternals 0x61000107f280
(SDWebImageManager) $12 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006080006406f0 0 elements
  _runningOperations = 0x00006080010671c0 @"1 element"
}

(lldb) pinternals 0x000060000106ef00
(SDWebImageManager) $8 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006000006524b0 0 elements
  _runningOperations = 0x0000600001277f80 @"1 element"
}

2. 执行预加载

  • 首先会取消掉所有的预加载, 所以确保这个方法不要被频繁调用.
  • 记录当前的时间, Tips:使用的是CFAbsoluteTimeGetCurrent()比较高效的获取时间的方法, 虽然后面好像没用到这个属性.
  • 对每个url调用一次使用startPrefetchingAtIndex方法, 在该方法中使用SDWebImageManager执行下载URL, 缓存也是在SDWebImageManager中做的, 详细可以参考SDWebImageManager内容.
  • 需要注意的是, 为了避免队列中由于某些未知原因导致某个请求未被调用, 最终导致无法完全结束, 在startPrefetchingAtIndex中一个URL缓存完成之后,方法中有如下一段代码, 目的是通过强制执行下一个请求缓存的目的来增加self.requestedCount的值, 已达到处理这种弄异常的目的, 但是一般情况下不会只想到这里面来.
if (self.prefetchURLs.count > self.requestedCount) {
    dispatch_async(self.prefetcherQueue, ^{
        [self startPrefetchingAtIndex:self.requestedCount];
    });
}

3. 取消

  • 情况当前所有的记录, 并使用持有的SDWebImageManager结束正在下载的任务.

3. 小结

这一个模块大部分是依靠SDWebImageManager来完成主体功能, 我曾经在某篇博客上看到有人说SDWebImagePrefetcher是不支持并发的, 至少在目前这个版本看来, 是完全支持一组URL并发的, 但是不支持同时预加载多组URL.

SDWebImage 子模块 GIF -- FLAnimatedImage

SDWebImage 支持动态图的, 建立在Flipboard的开源项目FLAnimatedImage的基础之上, 增加的一个扩展, 使用方法是pod 'SDWebImage/GIF', 或者手动把SDWebImage文件夹中的FLAnimatedImage文件夹拖入工程.

1. 接口定义

1. 加载Gif

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

2. 分析

1. 加载Gif

  • 直接调用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]方法,这个方法在最开始对UIView+WebCache模块有介绍.
  • 回调结果中有UIImage *image, NSData *imageData, 如果imageData是Gif, 使用[FLAnimatedImage animatedImageWithGIFData:imageData]方法初始化gif并使用.
  • 注意:[FLAnimatedImage animatedImageWithGIFData:imageData]方法耗时比较长, 本模块又是在主线程做这个操作, 假如下载Gif的同时, 用户在进行UI操作, 比如滑动页面等会造成掉帧, 可以将这一步丢到后台线程完成, 完成后在主线程进行展示. 这样做的下一个问题是, 下载GIF是在后台, SD下载完成回调丢回主线程, 在主线程丢到后台去生成一个FLAnimatedImage实例, 再回到主线程进行展示, 中间本不该回到主线程造成资源浪费. 前面说了, 这个模块式通过调用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]来完成下载操作, 我们可以自己调用该方法(不需要引入SDWebImage/GIF子模块). 例子如下(self.gifImageView 是一个FLAnimatedImageView实例):
#import "NSData+ImageContentType.h"
#import "UIView+WebCache.h"

[self.gifImageView sd_internalSetImageWithURL:[NSURL URLWithString:self.resource.previewImageUrl]
                             placeholderImage:nil
                                      options:0
                                 operationKey:nil
                                setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
                                    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                                    if (imageFormat == SDImageFormatGIF) {
                                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                            FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];;
                                            dispatch_async(dispatch_get_main_queue(), ^{
                                                weakSelf.gifImageView.animatedImage = animatedImage;
                                            });
                                        });
                                        weakSelf.gifImageView.image = nil;
                                    } else {
                                        weakSelf.gifImageView.image = image;
                                        weakSelf.gifImageView.animatedImage = nil;
                                    }
                                }
                                     progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                                     }];

3. 小结

这个模块被应该做三件事情, 一件是下载, 这个在下载模块完成了; 第二个是从下载下来的二进制文件中生成一张图片, 这个在UIImage+MultiFormat模块中完成的, 有兴趣的同学可以看看这个文件; 第三个是展示二进制文件, 这个是FLAnimatedImage做的.


写在最后

最近想看一下一些优秀的开源库是如何编写的, SDWebImage是我看的第一份源码(以前草草看的不算), 受益匪浅. 这次我边看边写笔记, 最终整理这篇博客, 不仅仅是对源码的流程讲解, 有一些小的细节小技巧我也有单独标出来. 平时码代码的过程还是太随意了, 因为工程的量级决定不需要太注重一些细节, 但是对于这些细节, 能注意的还是应该注意.

作者:wyanassert

原地址:https://github.com/wyanassert/WYBlob/blob/master/doc/SDWebImage/Analyze.md

你可能感兴趣的:(SDWebImage 分析)