前言:
IOS SDWebImage 2.X源码阅读(一)
IOS SDWebImage 2.X源码阅读(二)
IOS SDWebImage 2.X源码阅读(三)
IOS SDWebImage 2.X源码阅读(四)
(一)、SDWebImage 的使用
1、我们在项目中使用SDWebImage加载图片,用的最多的方法是UIImageView的category中的方法
[_iconImgView sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:[UIImage imageNamed:@"icon"]];
(二)、SDWebImage 的流程
(1)跟踪 sd_setImageWithURL:placeholderImage:options:progressBlock:completed方法,最终走的是如下:
– (void)sd_setImageWithURL:(NSURL*)url placeholderImage:(UIImage*)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
该方法中,它要保证没有当前正在进行异步下载操作,不会与即将进行的操作发生冲突,取消先前下载任务
[self sd_cancelCurrentImageLoad];
当时我还疑惑,对于一般的UIImageView下载图片时,这个取消当前下载的操作,重复了吧,因为在后面 [self sd_setImageLoadOperation:operation forKey:@”UIImageViewImageLoad”];方法中也有取消的操作,为啥还要呢,后来想到可能会有以下两种情况
1、我们人为的给某个UIImageView,加载两次图片
[imgView sd_setImageWithURL:[NSURL URLWithString:self.url1] placeholderImage:[UIImage imageNamed:@”icon”]];
[imgView sd_setImageWithURL:[NSURL URLWithString:self.url2] placeholderImage:[UIImage imageNamed:@”icon”]];
针对这种情况,我们希望是现实url2的图片,但是可能url1在url2之后下载下来,那么会覆盖掉url2,这不是我们想要的结果
…………………………………………………………………………………………………………
2、当tableview重用到某个cell时
我们也是希望显示的是当前显示在屏幕上的cell(即复用的cell)加载的url图片,不是之前的
还有一个优化网络请求,网络请求也很耗时,取消不必要的网络请求
sd_cancelCurrentImageLoad中又调用了如下方法
– (void)sd_cancelImageLoadOperationWithKey:(NSString *)key ;//取消图片加载操作
我们看到该方法中有个operationDictionary,看定义是各种operation的字典,代码没啥
/*
给UIView通过关键字loadOperationKey关联了一个operations,可以看出NSMutableDictionary中存放的是 {UIImageViewImageLoad = "";}
*/
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
回到刚才的sd_cancelImageLoadOperationWithKey:key:方法中去
id operations = [operationDictionary objectForKey:key];
//我们根据operationDictionary方法,通过key:UIImageViewImageLoad
继续看下面的代码,如何取消图片加载操作的呢
if (operations) {
/*
operations表示一个数组(NSArray)时,主要是考虑到gif这种动态图。因为gif是多张图片集合,所以需要NSArray类型来表示。
*/
if ([operations isKindOfClass:[NSArray class]]) {
//SDWebImageOperation数组 可理解为针对一个UIImageView要下载多个图片,eg:gif图像,或者在给一个UIImageView设置两次下载图片,那么就需要先cancle前一个
AppLog(@"gif --> [operation cancel];");
for (id operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
AppLog(@"SDWebImageOperation协议 [operation cancel];");
//实现SDWebImageOperation协议
[(id) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
回到sd_setImageWithURL:placeholderImage:options:progress:completed方法中
objc_setAssociatedObject(self, &imageURLKey, url,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
UIImageView当前这个对象添加一个imageURLKey的关联对象url。相当于现在这个图片的url属性绑定到了UIImageView对象上
下面有个if判断,设置占位图(默认图片)
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;//占位图
});
我们看下SDWebImageDelayPlaceholder
/**
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
默认情况下,占位符图像在加载图像时加载。 该标志将延迟加载占位符图像,直到图像加载完成。
*/
SDWebImageDelayPlaceholder = 1 << 9
1 << 9 :表示二进制的1左移9位–> 1000000000
………………………………………………………………
(options & SDWebImageDelayPlaceholder )这个bool值是多少呢
options:默认我们是1,那么表达式就是如下
00000000001
&1000000000
————————
00000000000 结果为false
& 两个为真才是真
………………………………………………………………
那么 if (!(options & SDWebImageDelayPlaceholder))这个表达式为真,
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
dispatch_main_async_safe这个宏如果当前是主进程,就直接执行block,否则把block放到主进程运行。所有的UI操纵需要放在主线中
下面判断url不为空的情况下,进行相关下载操作
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];//添加菊花等待
}
看下图片下载方法,调用了SDWebImageManager的
– (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
怎么下载的我们等会再看,我们先看下下载完成的block
[wself removeActivityIndicator];//移除菊花
if (!wself) return; //如果imageView不存在了就return停止操作
dispatch_main_sync_safe(^{ ………………});//之前说过图像的绘制只能在主线程完成:
在dispatch_main_sync_safe主线程进行ui的相关操作
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock){
completedBlock(image, error, cacheType, url);
return;
}
我们先看下SDWebImageAvoidAutoSetImage
/**
* By default, image is added to the imageView after download. But in some cases, we want to
* have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
* Use this flag if you want to manually set the image in the completion when success
默认情况下,下载后图像被添加到imageView。 但在某些情况下,我们要设置图像(应用过滤器或交叉淡入淡出动画实例添加它),如果你想手动设置图像在完成时使用此标志
*/
SDWebImageAvoidAutoSetImage = 1 << 11
该表达式可以理解为:当有image & 下载图片时设置了SDWebImageAvoidAutoSetImage & 完成了下载操作block
else if (image) {
wself.image = image;//设置图片
[wself setNeedsLayout];//刷新UI
} else {
if ((options & SDWebImageDelayPlaceholder)) {
//设置占位图
wself.image = placeholder;
[wself setNeedsLayout];
}
}
/**
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
默认情况下,占位符图像在加载图像时加载。 该标志将延迟加载占位符图像,直到图像加载完成。
*/
// SDWebImageDelayPlaceholder = 1 << 9,
//下载完成block回调 & 下载完成
if (completedBlock && finished) {
//block传入image
completedBlock(image, error, cacheType, url);
}
看下这个方法,该方法也调用了一进入该方法就调用的取消操作 [self sd_cancelImageLoadOperationWithKey:key];
[self sd_setImageLoadOperation:operation
forKey:@”UIImageViewImageLoad”];
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
AppLog(@"self-->%@",self);
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
//给operationDictionary添加一个key为"UIImageViewImageLoad"的id变量
}
在给的url为空的时候,移除菊花,将相关的错误放在回调的block中
else {
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
(2)在sd_setImageWithURL方法中又调用了SDWebImageManager类中
– (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
先是对url进行各种判断
if ([url isKindOfClass:NSString.class]) {//防止给的url是字符串不是NSURL
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL防止应用程序在参数类型错误(如发送NSNull而不是NSURL)时崩溃
if (![url isKindOfClass:NSURL.class]) {//url是不是NSURL的
url = nil;
}
__block SDWebImageCombinedOperation *operation =
[SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
……………………………………………………………………………………
__block 修饰变量operation,可以让它在block中修改它的值
__weak 是可以避免循环引用,但是会导致外部对象释放后,block内部也访问不到对象的问题,我们可以在block的内部声明一个__strong的变量来指向weakOperation,这样外部既能在block内部保持住,又能避免循环引用
self.failedURLs中存放的是失效的url,若是该url存在该列表中,那么SDWebImage就不会继续对应进行加载
//判断url是不是失效的url
//failedURLs:存储失效的url列表
BOOL isFailedUrl = NO;
// @synchronized加锁,避免多个线程执行同一段代码,
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//url为nil || 我们设置了SDWebImageRetryFailed(当URL无法下载时,该URL不会被列入黑名单会继续下载)& 是失效的url
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
//失败处理
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
将所有的下载任务都放到一个数组中去self.runningOperations,方便取消相关下载
//加锁,runningOperations存储所有的下载任务操作,
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
下载之前呢,它会先从缓存中查找,首先从内存查找,然后在磁盘缓存查找,找到了就不用去下载了,我们看下查找完成后的回调block,等下看查找过程
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key
done:^(UIImage *image, SDImageCacheType cacheType);
//operation取消,将下载任务从下载队列中移除
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
之前我们调用- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key中[operation cancel];方法,就有取消的操作,这实际上是个operation,看下它的取消方法,可以看到self.cancelled = YES;
//SDWebImageCombinedOperation遵循SDWebImageOperation,实现cacle()
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];//调用自身的cancel()
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
_cancelBlock = nil;
}
}
继续看下面的代码,有个if判断
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//……………………………………
}
我们看后半部分的这个,代码中貌似没有地方实现该方法,可以认为后半部分为true
(![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])
前半部分:(!image || options & SDWebImageRefreshCached) 没有找到缓存的imgae || 我们设置了SDWebImageRefreshCached
一进入这个if判断,又有一个if判断
if (image && options & SDWebImageRefreshCached) {}
如果图片在缓存中找到,但是options中有SDWebImageRefreshCached
那么就尝试重新下载该图片,这样是NSURLCache有机会从服务器端刷新自身缓存。
终于快要到“真正”下载的代码了,这部分代码我们等会看,先看下若是从缓存中查找到图片的处理
else if (image) {
// 从缓存中获取到了图片,而且不需要刷新缓存的
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
//加锁,移除相关操作
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
// Image not in cache and download disallowed by delegate又没有从缓存中获取到图片,shouldDownloadImageForURL又返回NO,不允许下载
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];