IOS SDWebImage 2.X源码阅读(一)

前言:
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];
            }
        }
    }];

你可能感兴趣的:(IOS)