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
解释:
每个 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都移除掉之后, 会调用父类NSOperation
的cancel
方法, 这会将isCancelled
属性置为YES, 在start方法调用的时候就不会真正执行. 最后调用[self.dataTask cancel];
关闭数据传输.
Question: 手动调cancel方法后, 就不会执行失败的block了吗?
e. 暂停下载
- 直接调用
(self.downloadQueue).suspended = suspended;
, 这儿利用了NSOperationQueue
的功能.
f. 取消所有的下载
[self.downloadQueue cancelAllOperations];
- 自动调用
SDWebImageDownloaderOperation
的cancel
方法.
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
- 在这儿将
progressBlock
和completedBlock
两个block都复制了一份,再存储到_callbackBlocks
中. - 在这儿也使用了
dispatch_barrier_async
方法, 这是个异步操作
c. 开始(继承父类)
- 注意, 这儿虽然覆盖了父类的
start
方法, 但是不能调用[super start]; - 在
SDWebImage
中, Operation被加到SDWebImageDownloader
的downloadQueue
中后会被自动执行, (自动调用operation
的start
方法) - 首先判断自己是否被取消了
- 再判断
self.unownedSession
是否还在, 一般情况下是还在的, 因为默认的SDWebImageDownloader
是个单例不会被释放, 但如果开发者自己初始化一个SDWebImageDownloader
就会存在self.unownedSession
不再引用一个session的情况. - 根据
session
和request
生成一个dataTask
, 并将自己标记为正在执行. - 开始下载, 并触发第一次'progressBlock'.
d. 取消(继承父类)
- 若已经完成, 直接返回.
- 调用[super cancel], 会将
isFinished
标记为YES. - 取消下载操作.
e. 是否在执行(继承父类)
- 用KVO通知值改变.
f. 是否已结束(继承父类)
- 同上.
g. 取消单个操作
- 根据token将对应的回调从``删除, 在这儿使用了
[NSArray removeObjectIdenticalTo:]
方法, 利用"本体性"而不是"相等性"去移除对应的回调, 个人猜测是为了提高查找的速度. 具体可以参考Equality这篇文章. - 在这儿使用了
dispatch_barrier_sync
, 注意这儿是一个同步方法, 后面根据移除后_callbackBlocks
是否为空判断是否要停止当前的下载.
Tips: start
与cancel
用@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