一行行看SDWebImage源码(一)

作者: 杨千嬅染了红头发 | 来源:发表于2016-05-24 16:37 被阅读9155次
    jay.jpg ```
    typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
    * The image wasn't available the SDWebImage caches, but was downloaded from the web.
    该图像是不可用的SDWebImage缓存,但是从网络下载的.
    /
    SDImageCacheTypeNone,
    /
    *
    * The image was obtained from the disk cache.
    图像从磁盘高速缓存获得.
    /
    SDImageCacheTypeDisk,
    /
    *
    * The image was obtained from the memory cache.
    图像从存储器高速缓存获得
    */
    SDImageCacheTypeMemory
    };
    
    SDWebImage是iOS开发者经常使用的一个开源框架,这个框架的主要作用是:
    一个异步下载图片并且支持缓存的UIImageView分类.
    ##UIImageView+WebCache
    我们最常用的方法就是这个:
    

    [_fineImageView sd_setImageWithURL:picURL placeholderImage:nil];

    
    现在开始我们一步步地看这个方法的内部实现:
    
    • (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
      [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
      }```

    这里会调用下面这个方法:

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

    我们看UIImageView+WebCache.h文件,我们可以发现为开发者提供了很多类似于- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder的方法.
    这些方法最终都会调用- sd_setImageWithURL: placeholderImage: options: progress: completed:,这个方法可以算是核心方法,下面我们看一下- sd_setImageWithURL: placeholderImage: options: progress: completed:
    方法的实现:

    首先执行

    //移除UIImageView当前绑定的操作.当TableView的cell包含的UIImageView被重用的时候首先执行这一行代码,保证这个ImageView的下载和缓存组合操作都被取消
    [self sd_cancelCurrentImageLoad];
    

    接下来我们来看看[self sd_cancelCurrentImageLoad]内部是怎么执行的:

    - (void)sd_cancelCurrentImageLoad {
        [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
    }
    //然后会调用UIView+WebCacheOperation的
    - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key
    

    UIView+WebCacheOperation

    下面我们先来看看UIView+WebCacheOperation里面都写了些什么:
    UIView+WebCacheOperation这个分类提供了三个方法,用于操作绑定关系

    #import <UIKit/UIKit.h>
    #import "SDWebImageManager.h"
    
    @interface UIView (WebCacheOperation)
    /**
     *  Set the image load operation (storage in a UIView based dictionary)
    设置图像加载操作(存储在和UIView做绑定的字典里面)
     *
     *  @param operation the operation
     *  @param key       key for storing the operation
     */
    - (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;
    
    /**
     *  Cancel all operations for the current UIView and key
      用这个key找到当前UIView上面的所有操作并取消
     *
     *  @param key key for identifying the operations
     */
    - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;
    
    /**
     *  Just remove the operations corresponding to the current UIView and key without cancelling them
    
     *
     *  @param key key for identifying the operations
     */
    - (void)sd_removeImageLoadOperationWithKey:(NSString *)key;
    
    

    为了方便管理和找到视图正在进行的一些操作,WebCacheOperation将每一个视图的实例和它正在进行的操作(下载和缓存的组合操作)绑定起来,实现操作和视图一一对应关系,以便可以随时拿到视图正在进行的操作,控制其取消等,如何进行绑定我们在下面分析:
    UIView+WebCacheOperation.m文件内
    - (NSMutableDictionary *)operationDictionary用到了<objc/runtime.h>中定义的两个函数:

    • objc_setAssociatedObject
    • objc_getAssociatedObject

    NSObject+AssociatedObject.h

    @interface NSObject (AssociatedObject)
    @property (nonatomic, strong) id associatedObject;
    @end
    

    NSObject+AssociatedObject.m

    @implementation NSObject (AssociatedObject)
    @dynamic associatedObject;
    -(void)setAssociatedObject:(id)object{
    objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    -(id)associatedObject { 
    return objc_getAssociatedObject(self, @selector(associatedObject));
    }
    

    objc_setAssociatedObject作用是**对已存在的类在扩展中添加自定义的属性 **,通常推荐的做法是添加属性的key最好是static char类型的,通常来说该属性的key应该是常量唯一的.
    objc_getAssociatedObject根据key获得与对象绑定的属性.

    - (NSMutableDictionary *)operationDictionary {
    /*
    这个loadOperationKey 的定义是:static char loadOperationKey;
    它对应的绑定在UIView的属性是operationDictionary(NSMutableDictionary类型)
    operationDictionary的value是操作,key是针对不同类型视图和不同类型的操作设定的字符串
    注意:&是一元运算符结果是右操作对象的地址(&loadOperationKey返回static char loadOperationKey的地址)
    */
        NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    //如果可以查到operations,就rerun,反正给视图绑定一个新的,空的operations字典
        if (operations) {
            return operations;
        }
        operations = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
    
    - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
        // 取消正在下载的队列
        NSMutableDictionary *operationDictionary = [self operationDictionary];
    //如果 operationDictionary可以取到,根据key可以得到与视图相关的操作,取消他们,并根据key值,从operationDictionary里面删除这些操作
        id operations = [operationDictionary objectForKey:key];
        if (operations) {
            if ([operations isKindOfClass:[NSArray class]]) {
                for (id <SDWebImageOperation> operation in operations) {
                    if (operation) {
                        [operation cancel];
                    }
                }
            } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
                [(id<SDWebImageOperation>) operations cancel];
            }
            [operationDictionary removeObjectForKey:key];
        }
    }
    

    接下来我们继续探索
    - sd_setImageWithURL: placeholderImage: options: progress: completed:

    - (void)sd_setImageWithURL:(NSURL *)url
              placeholderImage:(UIImage *)placeholder
                       options:(SDWebImageOptions)options
                      progress:(SDWebImageDownloaderProgressBlock)progressBlock 
    completed:(SDWebImageCompletionBlock)completedBlock {
        [self sd_cancelCurrentImageLoad];
        //将 url作为属性绑定到ImageView上,用static char imageURLKey作key
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    /*options & SDWebImageDelayPlaceholder这是一个位运算的与操作,!(options & SDWebImageDelayPlaceholder)的意思就是options参数不是SDWebImageDelayPlaceholder,就执行以下操作   
    
    #define dispatch_main_async_safe(block)\
        if ([NSThread isMainThread]) {\
            block();\
        } else {\
            dispatch_async(dispatch_get_main_queue(), block);\
        }
    */
    这是一个宏定义,因为图像的绘制只能在主线程完成,所以dispatch_main_sync_safe就是为了保证block在主线程中执行
     if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
    //设置imageView的placeHolder
                self.image = placeholder;
            });
        }
        
        if (url) {
    
            // 检查是否通过`setShowActivityIndicatorView:`方法设置了显示正在加载指示器。如果设置了,使用`addActivityIndicator`方法向self添加指示器
            if ([self showActivityIndicatorView]) {
                [self addActivityIndicator];
            }
    
            __weak __typeof(self)wself = self;
    //下载的核心方法
            id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options 
    progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
              //移除加载指示器
                [wself removeActivityIndicator];
              //如果imageView不存在了就return停止操作
                if (!wself) return;
                dispatch_main_sync_safe(^{
                    if (!wself) return;
    /*
    SDWebImageAvoidAutoSetImage,默认情况下图片会在下载完毕后自动添加给imageView,但是有些时候我们想在设置图片之前加一些图片的处理,就要下载成功后去手动设置图片了,不会执行`wself.image = image;`,而是直接执行完成回调,有用户自己决定如何处理。
    */
                    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                    {
                        completedBlock(image, error, cacheType, url);
                        return;
                    }
    /*
    如果后两个条件中至少有一个不满足,那么就直接将image赋给当前的imageView
    ,并调用setNeedsLayout
    */
                    else if (image) {
                        wself.image = image;
                        [wself setNeedsLayout];
                    } else {
    /*
    image为空,并且设置了延迟设置占位图,会将占位图设置为最终的image,,并将其标记为需要重新布局。
      */
                        if ((options & SDWebImageDelayPlaceholder)) {
                            wself.image = placeholder;
                            [wself setNeedsLayout];
                        }
                    }
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
     // 为UIImageView绑定新的操作,以为之前把ImageView的操作cancel了
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } else {
     // 判断url不存在,移除加载指示器,执行完成回调,传递错误信息。
            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);
                }
            });
        }
    }
    

    SDWebImageManager

    -sd_setImageWithURL:forState:placeholderImage:options:completed:中,下载图片方法是位于SDWebImageManager类中- downloadImageWithURL: options:progress:completed:函数
    我们看下文档是对SDWebImageManager怎么描述的

    /**
     * The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.
     * It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).
     * You can use this class directly to benefit from web image downloading with caching in another context than
     * a UIView.
     *
     * Here is a simple example of how to use SDWebImageManager:
     *
     * @code
    /*
    概述了SDWenImageManager的作用,其实UIImageVIew+WebCache这个Category背后执行操作的就是这个SDWebImageManager.它会绑定一个下载器也就是SDwebImageDownloader和一个缓存SDImageCache
    */
    
    
    /**
     * Downloads the image at the given URL if not present in cache or return the cached version otherwise.
       若图片不在cache中,就根据给定的URL下载图片,否则返回cache中的图片 
     *
     * @param url            The URL to the image
     * @param options        A mask to specify options to use for this request
     * @param progressBlock  A block called while image is downloading
     * @param completedBlock A block called when operation has been completed. 
     *
     *   This parameter is required. 
     * 
     *   This block has no return value and takes the requested UIImage as first parameter.
     *   In case of error the image parameter is nil and the second parameter may contain an NSError.
     *
     *   The third parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
     *   or from the memory cache or from the network.
     *
     *   The last parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is 
     *   downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
     *   block is called a last time with the full image and the last parameter set to YES.
     *
     * @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
     */
    
    /*
    *第一个参数是必须的,就是image的url
    *第二个参数options你可以定制化各种各样的操作
    *第三个参数是一个回调block,用于图片下载过程中的回调
    *第四个参数是一个下载完成的回调,会在图片下载完成后回调
    *返回值是一个NSObject类,并且这个NSObject类是遵循一个协议这个协议叫SDWebImageOperation,这个协议里面只写了一个协议,就是一个cancel一个operation的协议
    */
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
    

    我们继续看SDWebImageManager .m
    初始化方法

    /*
    *初始化方法
    *1.获得一个SDImageCache的单例
    *2.获得一个SDWebImageDownloader的单例
    *3.新建一个MutableSet来存储下载失败的url
    *4.新建一个用来存储下载operation的可遍数组
    */
    - (id)init {
        if ((self = [super init])) {
            _imageCache = [self createCache];
            _imageDownloader = [SDWebImageDownloader sharedDownloader];
            _failedURLs = [NSMutableSet new];
            _runningOperations = [NSMutableArray new];
        }
        return self;
    }
    

    利用image的url生成一个缓存时需要的key,cacheKeyFilter的定义如下:
    @property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
    typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);
    是一个可以返回一个字符串的block

    //如果检测到cacheKeyFilter不为空的时候,利用cacheKeyFilter来生成一个key
    //如果为空,那么直接返回URL的string内容,当做key.
    - (NSString *)cacheKeyForURL:(NSURL *)url {
        if (self.cacheKeyFilter) {
            return self.cacheKeyFilter(url);
        }
        else {
            return [url absoluteString];
        }
    }
    

    检查一个图片是否被缓存的方法

    - (BOOL)cachedImageExistsForURL:(NSURL *)url {
      //调用上面的方法取到image的url对应的key
        NSString *key = [self cacheKeyForURL:url];
    //首先检测内存缓存中时候存在这张图片,如果已有直接返回yes 
        if ([self.imageCache imageFromMemoryCacheForKey:key] != nil) return YES;
    //如果内存缓存里面没有这张图片,那么就调用diskImageExistsWithKey这个方法去硬盘找
        return [self.imageCache diskImageExistsWithKey:key];
    }
    
    // 检测硬盘里是否缓存了图片
    - (BOOL)diskImageExistsForURL:(NSURL *)url {
     NSString *key = [self cacheKeyForURL:url];
     return [self.imageCache diskImageExistsWithKey:key];
    }
    

    下面两个方法比较类似,都是先根据图片的url创建对应的key

    第一个方法先用BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);判断图片有没有在内存缓存中,如果图片在内存缓存中存在,就在主线程里面回调block,如果图片没有在内存缓存中就去查找是不是在磁盘缓存里面,然后在主线程里面回到block

    第二个方法只查询图片是否在磁盘缓存里面,然后在主线程里面回调block

    - (void)cachedImageExistsForURL:(NSURL *)url
                         completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
        NSString *key = [self cacheKeyForURL:url];
        
        BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
        
        if (isInMemoryCache) {
            // making sure we call the completion block on the main queue
            dispatch_async(dispatch_get_main_queue(), ^{
                if (completionBlock) {
                    completionBlock(YES);
                }
            });
            return;
        }
        
        [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
            // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
            if (completionBlock) {
                completionBlock(isInDiskCache);
            }
        }];
    }
    
    - (void)diskImageExistsForURL:(NSURL *)url
                       completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
        NSString *key = [self cacheKeyForURL:url];
        
        [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
            // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
            if (completionBlock) {
                completionBlock(isInDiskCache);
            }
        }];
    }
    

    上面的方法都是用于查询图片是否在内存缓存或磁盘的缓存下面的方法可以算是SDWebImageManager核心方法:

    //通过url建立一个operation用来下载图片.
    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock
    
     //防止开发者把传入NSString类型的url,如果url的类型是NSString就给转换成NSURL类型
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        //如果转换NSURL失败,就把传入的url置为nil下载停止
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    

    首先我们先来看看__block__weak的区别
    __block用于指明当前声明的变量在被block捕获之后,可以在block中改变变量的值.因为在block声明的同时会截获该block所使用的全部自动变量的值,这些值只在block中只有"使用权"而不具有"修改权".而__block说明符就为block提供了变量的修改权,__block不能避免循环引用,这就需要我们在 block 内部将要退出的时候手动释放掉 blockObj,blockObj = nil

    __weak是所有权修饰符,__weak本身是可以避免循环引用的问题的,但是其会导致外部对象释放之后,block内部也访问不到对象的问题,我们可以通过在block内部声明一个__strong的变量来指向weakObj,使外部既能在block内部保持住又能避免循环引用

      __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
       __weak SDWebImageCombinedOperation *weakOperation = operation;
    

    我们再来看看SDWebImageCombinedOperation到底有一些什么内容
    SDWebImageCombinedOperation它什么也不做,保存了两个东西(一个block,可以取消下载operation,一个operation,cacheOperation用来下载图片并且缓存的operation)
    并且SDWebImageCombineOperation遵循<SDWebImageOperation>协议,所以operation可以作为返回值返回

    @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
    
    @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
    @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
    @property (strong, nonatomic) NSOperation *cacheOperation;
    
    @end
    
    @implementation SDWebImageCombinedOperation
    
    - (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
        // check if the operation is already cancelled, then we just call the cancelBlock
        if (self.isCancelled) {
            if (cancelBlock) {
                cancelBlock();
            }
            _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
        } else {
            _cancelBlock = [cancelBlock copy];
        }
    }
    
    - (void)cancel {
        self.cancelled = YES;
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.cancelBlock) {
            self.cancelBlock();
            
            // TODO: this is a temporary fix to #809.
            // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
    //        self.cancelBlock = nil;
            _cancelBlock = nil;
        }
    }
    
    @end
    

    @synchronized是OC中一种方便地创建互斥锁的方式--它可以防止不同线程在同一时间执行区块的代码
    self.failedURLs是一个NSSet类型的集合,里面存放的都是下载失败的图片的url,failedURLs不是NSArray类型的原因是:
    在搜索一个个元素的时候NSSet比NSArray效率高,主要是它用到了一个算法hash(散列,哈希) ,比如你要存储A,一个hash算法直接就能找到A应该存储的位置;同样当你要访问A的时候,一个hash过程就能找到A存储的位置,对于NSArray,若想知道A到底在不在数组中,则需要遍历整个数据,显然效率较低了
    并且NSSet里面不含有重复的元素,同一个下载失败的url只会存在一个
    - (BOOL)containsObject:(ObjectType)anObject;,判断集合里面是否含有这个obj

        BOOL isFailedUrl = NO;
    //创建一个互斥锁防止现有的别的线程修改failedURLs
    //判断这个url是否是fail过的,如果url failed过的那么isFailedUrl就是true. 
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    
    //如果url不存在那么直接返回一个block,如果url存在那么继续
    //!(options & SDWebImageRetryFailed) 之前就提过一个类似的了,它的意思看这个options是不是和SDWebImageRetryFailed不相同
    //如果不相同并且isFailedUrl是true.那么就回调一个error的block
        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;
        }
    

    把operation加入到self.runningOperations的数组里面,并创建一个互斥线程锁来保护这个操作
    获取image的url对应的key

        @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
        NSString *key = [self cacheKeyForURL:url];
    

    其实看到这里,下面牵扯的代码就会越来越越多了,会牵扯到的类也越来越多,我们先一步步地顺着往下看,然后再看涉及到的类里面写了些什么,最后再做总结,在这之前如果你对NSOperation还不够了解建议你先暂时停下看看下这篇文章NSOperation. 然后再继续往下阅读.

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;SDImageCache的一个方法,根据图片的key,异步查询磁盘缓存的方法

    我们先来看下这个方法里面都有什么:
    _ioQueue的定义是:@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
    _ioQueue的初始化是:
    _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    DISPATCH_QUEUE_SERIAL代表的是创建一个串行的队列,所以_ioQueue是一个串行队列(任务一个执行完毕才执行下一个)
    PS:如果你对GCD队列不太了解可以先看下GCD使用经验与技巧浅谈,然后再继续阅读额

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key 
    done:(SDWebImageQueryCompletedBlock)doneBlock {
        if (!doneBlock) {
            return nil;
        }
    
        if (!key) {
            doneBlock(nil, SDImageCacheTypeNone);
            return nil;
        }
    
        // 1.首先查看内存缓存,如果查找到,则直接调用doneBlock并返回
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
    //2.如果内存中没有,则在磁盘中查找,如果找到,则将其放到内存缓存中,并调用doneBlock回调
        NSOperation *operation = [NSOperation new];
    //在ioQueue中串行处理所有磁盘缓存
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                return;
            }
    //创建自动释放池,内存及时释放
            @autoreleasepool {
      //根据图片的url对应的key去磁盘缓存中查找图片
                UIImage *diskImage = [self diskImageForKey:key];
    //如果可以在磁盘中查找到image,并且self.shouldCacheImagesInMemory = YES(默认是YES,if memory cache is enabled)就将image储存到内存缓存中
                if (diskImage && self.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
            //self.memCache是NSCache创建的一个对象,下面的方法是NSCache储存对象的方法,如果你对cost的作用不太了解可以看我另外一篇文章NSCache
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
      //最后在主线程里面调用doneBlock返回
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
    
        return operation;
    
     operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
            if (operation.isCancelled) {
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
    
                return;
            }
    
    //条件1:在缓存中没有找到图片或者options选项里面包含了SDWebImageRefreshCached(这两项都需要进行请求网络图片的)
    //条件2:代理允许下载,SDWebImageManagerDelegate的delegate不能响应imageManager:shouldDownloadImageForURL:方法或者能响应方法且方法返回值为YES.也就是没有实现这个方法就是允许的,如果实现了的话,返回YES才是允许
            if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
    //如果在缓存中找到了image且options选项包含SDWebImageRefreshCached,先在主线程完成一次回调,使用的是缓存中找的图片
                if (image && options & SDWebImageRefreshCached) {
                    dispatch_main_sync_safe(^{
    
                    // 如果在缓存中找到了image但是设置了SDWebImageRefreshCached选项,传递缓存的image,同时尝试重新下载它来让NSURLCache有机会接收服务器端的更新
                        completedBlock(image, nil, cacheType, YES, url);
                    });
                }
    
                // 如果没有在缓存中找到image 或者设置了需要请求服务器刷新的选项,则仍需要下载
                SDWebImageDownloaderOptions downloaderOptions = 0;
      
                //开始各种options的判断
                if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
                if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
                if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
                if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
                if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
                if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
                if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
                if (image && options & SDWebImageRefreshCached) {
                // 如果image已经被缓存但是设置了需要请求服务器刷新的选项,强制关闭渐进式选项
                    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                   // 如果image已经被缓存但是设置了需要请求服务器刷新的选项,忽略从NSURLCache读取的image
                    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
                }
    
                //创建下载操作,先使用self.imageDownloader下载
                id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (!strongOperation || strongOperation.isCancelled) {
                        // Do nothing if the operation was cancelled
                        //如果操作取消了,不做任何事情
                        // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                    //如果我们调用completedBlock,这个block会和另外一个completedBlock争夺一个对象,因此这个block被调用后会覆盖新的数据
                    }
                    else if (error) {
                        //进行完成回调
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                            }
                        });
                      //将url添加到失败列表里面
                        if (   error.code != NSURLErrorNotConnectedToInternet
                            && error.code != NSURLErrorCancelled
                            && error.code != NSURLErrorTimedOut
                            && error.code != NSURLErrorInternationalRoamingOff
                            && error.code != NSURLErrorDataNotAllowed
                            && error.code != NSURLErrorCannotFindHost
                            && error.code != NSURLErrorCannotConnectToHost) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
                        }
                    }
                    else {
                        //如果设置了下载失败重试,将url从失败列表中去掉
                        if ((options & SDWebImageRetryFailed)) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
                        }
                        
                        BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
            //options包含了SDWebImageRefreshCached选项,且缓存中找到了image且没有下载成功
                        if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                            // Image refresh hit the NSURLCache cache, do not call the completion block
     // 图片刷新遇到了NSSURLCache中有缓存的状况,不调用完成回调。
                    }
      //图片下载成功并且 设置了需要变形Image的选项且变形的代理方法已经实现
                        else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
    //全局队列异步执行                      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    
                                //调用代理方法完成图片transform
                                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
    
                                if (transformedImage && finished) {
                                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
    
                    //对已经transform的图片进行缓存
                                    [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                                }
                                //主线程执行完成回调
                                dispatch_main_sync_safe(^{
                                    if (strongOperation && !strongOperation.isCancelled) {
                                        completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                    }
                                });
                            });
                        }
    //如果没有图片transform的需求并且图片下载完成且图片存在就直接缓存
                        else {
                            if (downloadedImage && finished) {
                                [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                            }
                       //主线程完成回调 
                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        }
                    }
    
                    if (finished) {
           // 从正在进行的操作列表中移除这组合操作
                        @synchronized (self.runningOperations) {
                            if (strongOperation) {
                                [self.runningOperations removeObject:strongOperation];
                            }
                        }
                    }
                }];
              //设置组合操作取消得得回调
                operation.cancelBlock = ^{
                    [subOperation cancel];
                    
                    @synchronized (self.runningOperations) {
                        __strong __typeof(weakOperation) strongOperation = weakOperation;
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                };
            }
    
    
    //处理其他情况
    //case1.在缓存中找到图片(代理不允许下载 或者没有设置SDWebImageRefreshCached选项  满足至少一项)
            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];
                }
            }
              //case2:缓存中没有扎到图片且代理不允许下载
            else {
            //主线程执行完成回调
                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];
                }
            }
        }];
    

    总结:
    这个方法主要完成了这些工作:1.创建一个组合Operation,是一个SDWebImageCombinedOperation对象,这个对象负责对下载operation创建和管理,同时有缓存功能,是对下载和缓存两个过程的组合。
    2.先去寻找这张图片 内存缓存和磁盘缓存,这两个功能在self.imageCachequeryDiskCacheForKey: done:方法中完成,这个方法的返回值既是一个缓存operation,最终被赋给上面的OperationcacheOperation属性。在查找缓存的完成回调中的代码是重点:它会根据是否设置了SDWebImageRefreshCached
    选项和代理是否支持下载决定是否要进行下载,并对下载过程中遇到NSURLCache的情况做处理,还有下载失败的处理以及下载之后进行缓存,然后查看是否设置了形变选项并调用代理的形变方法进行对图片形变处理。
    3.将上面的下载方法返回的操作命名为subOperation,并在组合操作operationcancelBlock代码块中添加对subOperationcancel方法的调用。

    整个大体的操作流程,就是这些,你不感觉少点啥子麽,如何下载,如何缓存(缓存只是暂时看了两个相关的方法)我们在下一篇再详细地看

    相关文章

      网友评论

      • 高高叔叔:妈的大神都是这样写代码的这样理解的啊。。干的
        高高叔叔:@杨千嬅染了红头发 看的太仔细了,感觉和原作者差不多。
        杨千嬅染了红头发:@高高叔叔 什么意思
      • 洲洲哥:大神您好:

        渣渣这里有一个问题啊
        - (SDOperationsDictionary *)operationDictionary {
        /**
        ### 这里有一个疑问?
        这样创建出来的字典每一次都是新的
        虽然用了 static char loadOperationKey,但是没一次创建的地址都是新的,并且该字典还是空的。
        创建完成之后都直接返回了。不知道每次创建的都是新的并且还是空的字典,这样的开销会很大的??!!!,也希望各位看官给予解答啊???
        */
        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        // 创建成功返回该字典,直接把该字典当做属性返回
        if (operations) {
        return operations;
        }
        operations = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
        }]

        这里每次都创建一个新的字典出来,并且返回。这里有什么设计思路吗?或者其他的吗。这里每次就返回了一个新的字典,后面的函数都不会继续执行。这样有什么好处吗?
      • leaderleader:很详细
      • wg689:非常好
      • Jux2020:请问下SDWebImageOperation的cancel方法是怎么实现的?
      • 薐驚:very nice
      • 3f58e786b546::sweat_smile: 顶一下
      • 58fcd70a1c3b:MARK 太赞了
      • da27c260cc85:ImageManager里面得到一个operation在什么时候执行了呢? 貌似没有看到在哪里执行的.
      • da27c260cc85:__block在我印象中是可以避免循环引用的
      • Club丶夜:表示水平不够。整个思路没看懂。。 :fearful:
      • 记住你姓李:一行行看SDWebImage源码(一) 出来了 那 ( 二 ) 什么时候出啊
        杨千嬅染了红头发:@鬼话连篇 端午节吧...最近忙写web界面
      • 漆皮耳机线:博主 博主~~赶紧粗第二弹吧~~~看你写的感觉比自己闷头看清晰好多呐 谢谢啦!!
        杨千嬅染了红头发:@漆皮耳机线 端午节写....
        漆皮耳机线:@missummer 不要弃坑 不要弃坑 不要弃坑!!!!!久一点没关系 不要弃坑是重点啊!!!!!!!干巴爹!!!!
        杨千嬅染了红头发:@漆皮耳机线 还没开始。。太忙了这几天
      • iOSWoden:改天仔细阅读学习学习
      • pengxuyuan:问下博主 UIView+WebCacheOperation.h 这个类 绑定了有什么用呢 还有SDWebImageOperation.h这个协议 SD里面好像没有实现 这样子怎么cancel呢?
        杨千嬅染了红头发:@彭序猿 这只是其中一种情况,还有其他的看源码找吧
        pengxuyuan:@missummer 懂了 这里就是防止 上一步的下载还没完成。 现在在看SDWebImage这个框架 晚点我也想写点blog来记录先 还望多多指导。 :pray: :pray: :pray:
        杨千嬅染了红头发:@彭序猿 我建议你先去查一下NSOperation相关的资料,用google查,绑定的是下载和缓存的组合操作,下一篇应该会写,你想一下一个tableView滑动的时候,有时候你滑动的速度过快,部分cell会滚动出屏幕,并且此时它们对应的图片还没有下载(或者找到缓存),这些cell被加入到了一个NSset集合中,新的cell就是从这里面取出来的,所以为了确保新的cell上面没有历史图片 或者正在进行的操作,就需要 去调用[self sd_cancelCurrentImageLoad]; 绑定的话就是用的runtime的一个方法
      • R0b1n_L33:建议撸主把自己做了注释的项目工程分享出来
        杨千嬅染了红头发:@ljysdfz 还是自己看一遍看慢一点比较好额,结合着这个,理解更全面点
      • 零度15号:深入学习下
      • abb266389fd0:66666,Mark
      • xxttw:给力啊
      • 1b2ae550dc99:我觉着你要是打赏的钱,写1块,2块应该会有打赏的,你得考虑底层开发者的工资。。。哈哈
      • 橙子晓:不错
      • _____柠檬:不错不错
      • Jessica丶优雅:666666666666666666
      • 814077f6025e:找了很多SDWebImage的东西,这个一看就懂了,膜拜大神
      • Yapu:写的很详细. :+1: :+1: :+1:
      • Cosinx:最近刚好看到这一块,谢谢作者的无私奉献,顶顶顶 :pray:

      本文标题:一行行看SDWebImage源码(一)

      本文链接:https://www.haomeiwen.com/subject/hjpirttx.html