美文网首页将来跳槽用iOS接下来要研究的知识点
【源码解读】SDWebImage ─── 总结

【源码解读】SDWebImage ─── 总结

作者: WellsCai | 来源:发表于2018-01-03 21:01 被阅读9次

    SDWebImage是一个提供UIImageView和UIButton类异步加载图片并且缓存的框架,接口简洁,类的分工十分明确。框架文件也不少,但主要围绕下载缓存的实现去解读就容易理清楚。

    我们以UIImageView的下载为例来探究。

    UIImageView+WebCache

    当我们使用图片异步加载时,是用分类UIImageView+WebCache中的接口。该分类提供了多种设置图片的方式,但是最终都会调用如下方法(这也是一个常见的设计思想)。

    //设置图片
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
    

    当然,可以设置单张图片,也可以设置gif图片(多张图片)。也可以取消对应图片的下载。

    //设置gif图片(多张图片)
    - (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;
    //取消图片下载
    - (void)sd_cancelCurrentImageLoad;
    //取消gif图片下载(多张图片)
    - (void)sd_cancelCurrentAnimationImagesLoad;
    

    除了设置图片外,UIImageView+WebCache还提供设置菊花指示器的功能(UIActivityIndicatorView),以便在加载时可以控件中看到旋转的菊花指示器,起到提示用户的功能。

    //设置是否显示
    - (void)setShowActivityIndicatorView:(BOOL)show;
    //设置样式
    - (void)setIndicatorStyle:(UIActivityIndicatorViewStyle)style;
    

    了解完UIImageView+WebCache的功能,我们主要来看看怎么设置单张图片的?

    //设置单张图片的主要调用入口
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
        //取消当前的下载???
        [self sd_cancelCurrentImageLoad];
        //关联对象,相当于有个url保存在self中
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        //如果不是下载好再加载占位符的模式  就在主线程设置占位符
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                self.image = placeholder;
            });
        }
        
        if (url) {
    
            // 添加菊花
            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];
                //如果控制器被释放,就不用继续执行
                if (!wself) return;
                dispatch_main_sync_safe(^{
                    //如果控制器被释放,就不用继续执行
                    if (!wself) return;
                    
                    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)//有图片,非自动设置图片的模式,传出结果就好
                    {
                        completedBlock(image, error, cacheType, url);
                        return;
                    }
                    else if (image) {//有图片,自动设置图片
                        wself.image = image;
                        [wself setNeedsLayout];//必要吗?
                    } else {//没图片
                        //下载好再加载占位符的模式,设置占位图片(其他模式肯定已经设置好了)
                        if ((options & SDWebImageDelayPlaceholder)) {
                            wself.image = placeholder;
                            [wself setNeedsLayout];
                        }
                    }
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
            //UIView的operations字典保存UIImageViewImageLoad和UIImageViewAnimationImages的operation
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } 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);
                }
            });
        }
    }
    

    当给UIImageView设置单张图片时,会进行如下操作:
    ① 取消当前的下载操作。
    ② 通过动态关联,将imageURL保存起来,成为UIImageView的属性。
    ③ 根据传入的模式来设置占位符。
    ④ 通过判断url来决定返回错误或者通过url去获取,获取的话就是交给SDWebImageManager了。

    下载时,会将对应的下载操作保存起来,在UIView+WebCacheOperation中,动态关联了一个operations的字典,用来保存一个下载单张图操作,和一个下载图片组操作。之所以把保存operation的功能放在UIView的分类中,也是为了能让其他UIView的子类,比如UIButton使用。
    那为什么要保存对应的操作呢?我想作者是把UIImageView分成可以同时设置单张图片和图片组两种方式,这两张设置不冲突,但是你设置单张图片的时候,就必须把你上次设置单张图片的操作取消。

    //动态关联对象,解决分类创建不了属性的问题,提供operations属性的功能
    //这边有点类似于懒加载(先获取,如果没有再创建)
    - (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;
    }
    

    前面讲完了怎么设置图片的方法,最主要的通过url获取图片的方法(下载或者缓存)还是在SDWebImageManager中。

    SDWebImageManager

    SDWebImageManager是一个管理类,里面有这么两个重要的属性(缓存和下载器)。

    @property (strong, nonatomic, readwrite) SDImageCache *imageCache;
    @property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader;
    

    SDWebImageManager是一个单例对象,在创建时也自动创建了缓存和下载器两个对象。

    + (id)sharedManager {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }
    
    - (instancetype)init {
        SDImageCache *cache = [SDImageCache sharedImageCache];
        SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
        return [self initWithCache:cache downloader:downloader];
    }
    
    - (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader {
        if ((self = [super init])) {
            _imageCache = cache;
            _imageDownloader = downloader;
            _failedURLs = [NSMutableSet new];
            _runningOperations = [NSMutableArray new];
        }
        return self;
    }
    

    关于缓存和下载器具体的功能和设计可以看我的这两篇文章。
    【源码解读】SDWebImage ─── 缓存的设计
    【源码解读】SDWebImage ─── 下载器的设计

    SDWebImageManager提供的功能其实就是缓存和下载器的组合:你提供一个url,我去缓存(imageCache)中(先内存缓存再磁盘缓存)寻找,如果没找到就让下载器(imageDownloader)去下载。

    当我们通过SDWebImageManager获取图片时,调用的是:

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
    

    我们不仅需要传入url,下载中的回调progressBlock,完成后的回调completedBlock,还要传入options,这个options可以是单个也可以是多个,主要用来控制图片的设置过程(包括缓存和下载),比如传入SDWebImageAvoidAutoSetImage就需要我们手动去将下载完成后的图片赋值给UIImageView。可以说options是为了满足一些特殊的需求,当options为nil时就是一些通用的过程。

    typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
        SDWebImageRetryFailed = 1 << 0,//失败后重试
        SDWebImageLowPriority = 1 << 1,//低优先级,比如UIScrollow滚动时不下载
        SDWebImageCacheMemoryOnly = 1 << 2,//只进行内存缓存
        SDWebImageProgressiveDownload = 1 << 3,//渐进式下载,图片渐进式显示
        SDWebImageRefreshCached = 1 << 4,//刷新缓存
        SDWebImageContinueInBackground = 1 << 5,//后台下载
        SDWebImageHandleCookies = 1 << 6,//存储Cookies
        SDWebImageAllowInvalidSSLCertificates = 1 << 7,//允许非法证书
        SDWebImageHighPriority = 1 << 8,//放在高优先级的队列
        SDWebImageDelayPlaceholder = 1 << 9,//延迟占位图片出现的时间,等下载完成
        SDWebImageTransformAnimatedImage = 1 << 10,//gif的动画????
        SDWebImageAvoidAutoSetImage = 1 << 11//下载完不自动设置图片
    };
    

    接下来我们来看SDWebImageManager具体是怎么设计的?
    当我们传入url,下载中的回调progressBlock,完成后的回调completedBlock,以及相关的options来SDWebImageManager获取图片时,会进行如下操作(后面附上源码和解析)。
    ① 判断不可以下载的情况(url不可用,该url曾经下载失败过)
    这边也可以看出如果不是设置成SDWebImageRetryFailed,一旦url下载失败过,就会加入failedURLs,下次下载该url时,直接返回错误信息。

    ② 到这一步证明url可以下载。创建一个SDWebImageCombinedOperation对象,并添加进runningOperations中。
    SDWebImageCombinedOperation是一个组合操作(里面有cacheOperation的属性),后面会将imageCache查询获取的操作赋值给cacheOperation。
    runningOperations是为了取消操作,判断是否有正在进行的操作。

    ③ 将imageCache查询获取的操作赋值给组合操作operation中的cacheOperation,并将该operation返回给UIImageView的分类。

    在imageCache查询结果的回调中,主要分成三种情况:

    • 如果缓存中没有图片
      通过options去设置imageDownloader的downloaderOptions,然后通过imageDownloader创建一个下载队列subOperation。
      imageDownloader下载完成的回调又有以下三种情况:
      1)被取消,不需要做什么
      2)有错误,返回对应错误,并添加进failedURLs
      3)成功下载,没有下载图片的情况就是NSURLCache有缓存,有下载图片的情况,如果有实现转换图片的代理,就先转换,再通过imageCache储存起来,并且在主线程回调completedBlock。

    • 如果缓存中有图片
      在主线程回调completedBlock,将image传出去。将operation从runningOperations移除。

    • 如果缓存没有图片,下载操作也不被允许(通过代理来设置)
      在主线程回调completedBlock,将nil和SDImageCacheTypeNone传出去。将operation从runningOperations移除。

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
        /*  容错机制  */
        //completedBlock不能为空
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        //如果误传NSString类型,就转成NSURL类型
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        // 如果传的是其他乱七八糟的类型,比如NSNull,就置为nil
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
        //创建一个混合操作(其实是缓存操作)
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
        BOOL isFailedUrl = NO;
        //加锁,获取不可用集合里是否有包含该url
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    
        //如果url为空,或者(options为不是重新下载错误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;
        }
    
        //到这步,证明可以下载
        //加锁,添加到正在下载的集合runningOperations中
        @synchronized (self.runningOperations) {
            [self.runningOperations addObject:operation];
        }
        
        //通过url获取key(没设置过滤器的情况就是装成NSString)
        //过滤器的作用就是让用户来自定义装换的标准
        NSString *key = [self cacheKeyForURL:url];
    
        operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
            //如果操作被取消,就从当前运行的操作数组中剔除
            if (operation.isCancelled) {
                @synchronized (self.runningOperations) {
                    [self.runningOperations removeObject:operation];
                }
    
                return;
            }
    
    #pragma mark - 如果没有图片 || 需要更新缓存的类型 && 下载代理能响应方法
            if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                //如果有图片,但是是需要更新缓存的类型
                if (image && options & SDWebImageRefreshCached) {
                    dispatch_main_sync_safe(^{
                        // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                        // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                        completedBlock(image, nil, cacheType, YES, url);
                    });
                }
    
                // download if no image or requested to refresh anyway, and download allowed by delegate
                SDWebImageDownloaderOptions downloaderOptions = 0;
                if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;//低优先级
                if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;//渐进式下载
                if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;//使用NSURLCache
                if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;//后台下载
                if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;//使用cookies
                if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;//允许非法SSL
                if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;//高优先级
                //如果有图片,但是是需要更新缓存的类型
                if (image && options & SDWebImageRefreshCached) {
                    // 因为有图片了,只是更新缓存,所以关掉渐进式下载(减去该枚举)
                    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                    // 忽略 NSURLCache的响应(加上该枚举)
                    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
                }
                
                /* ===============下载的Block============== */
                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;
                    //1.被取消
                    if (!strongOperation || strongOperation.isCancelled) {
                        // Do nothing if the operation was cancelled
                        // See #699 for more details
                        // 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
                    }
                    else if (error) {//2.有错误
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                            }
                        });
    
                        if (   error.code != NSURLErrorNotConnectedToInternet//无连接
                            && error.code != NSURLErrorCancelled//连接取消
                            && error.code != NSURLErrorTimedOut//超出时间
                            && error.code != NSURLErrorInternationalRoamingOff//网络中断
                            && error.code != NSURLErrorDataNotAllowed//不允许数据
                            && error.code != NSURLErrorCannotFindHost//不能发现主地址
                            && error.code != NSURLErrorCannotConnectToHost) {//不能连接主地址
                            //如果不是以上特殊情况,就判定该url不可用
                            @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
                        }
                    }
                    else {//3.正常下载完成
                        //如果是重试错误的url,就从失败的url数组中剔除该url
                        if ((options & SDWebImageRetryFailed)) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
                        }
                        
                        //是否有缓存在磁盘
                        BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
    
                        // 重新下载选项 && 缓存有图片 && 没有下载图片
                        if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                            // Image refresh hit the NSURLCache cache, do not call the completion block
                        }
                        //如果有下载图片&&(不是gif)&&有响应图片转换的代理
                        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), ^{
                                //获取变形(转换)后的图片
                                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
    
                                //如果变形后图片有值且下载完成
                                if (transformedImage && finished) {
                                    //是否有变形
                                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                    //将图片储存起来,根据是否转换来决定是否重新计算size
                                    [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                                }
    
                                //操作没取消的话就在主线程回调completedBlock
                                dispatch_main_sync_safe(^{
                                    if (strongOperation && !strongOperation.isCancelled) {
                                        completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                    }
                                });
                            });
                        }
                        else {//有下载图片
                            //缓存图片
                            if (downloadedImage && finished) {
                                [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                            }
                            //操作没取消的话就在主线程回调completedBlock
                            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];
                            }
                        }
                    }
                }];
    
                /* ===============取消的Block============== */
                
                operation.cancelBlock = ^{
                    [subOperation cancel];
                    
                    @synchronized (self.runningOperations) {
                        __strong __typeof(weakOperation) strongOperation = weakOperation;
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                };
            }
    #pragma mark - 如果缓存中有图片
            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 {
    #pragma mark - 缓存没有图片,下载操作也不允许
                // Image not in cache and download disallowed by delegate
                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];
                }
            }
        }];
    
        return operation;
    }
    

    相关文章

      网友评论

        本文标题:【源码解读】SDWebImage ─── 总结

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