从sd_setImageWithURL:方法谈SDWebImage (二)

从sd_setImageWithURL:方法谈SDWebImage (一)
从sd_setImageWithURL:方法谈SDWebImage (二)

上篇文章从sd_setImageWithURL:方法谈到SDWebImageManager。
而SDWebImageManager在SDWebImage中的身份是属于协调管理的角色而非执行者。SDWebImageManager主要是协调SDImageCacheSDWebImageDownloader的单例对象。

SDImageCache *imageCache
SDWebImageDownloader *imageDownloader 
SDWebImage重要的组成

直接从GitHud上拉到作者提供的图。很直观的看到了SDWebImage下载图片的流程:

图解SDWebImage下载过程

  1. sd_setimageWithURL(),从最初调用设置图片方法。
  2. sd_internalSetImageWithURL(),所有设置图片的UIKit的分类最终会调用UIView+WebCache中的sd_internalSetImageWithURL()来下载图片。
  3. SDWebImageManager.sharedManager开启加载
    loadImageWithURL()方法。
  4. loadImageWithURL()中首先会调用SDImageCache对象的queryCacheOperationForKey()方法查找缓存,首先查找内存中是否有图片的缓存,如果没有继续查找磁盘缓存。
  5. 如果查找命中,返回image对象、和imageData。
  6. 内存和磁盘中都没有找到图片。使用SDWebImageDownloader对象开启图片下载任务。
  7. 返回SDWebImageDownloader对象图片下载的结果到SDWebImageManager。
  8. SDWebImageManager再调用SDImageCache对象将图片缓存值到内存和磁盘中(实际情况根据SDWebImageOptions的设置)。
  9. 将图片返回到UIView+WebCache中,调用completionBlock回调。
  10. 最后根据UI的控件根据自身的情况设置图片。在WebCache Categories文件夹中的分类,UIView+WebCache除了是用来下载,还使用默认或自定义block来设置图片到UI控件中。

上面的操作过程是SDWebImage主要的功能流程。其中1、2、3、9、10的步骤在前一篇的文章中从sd_setImageWithURL:方法谈SDWebImage 起有过介绍。
剩下的将会在本文中介绍。
SDWebImageManager中的核心方法如下,在下面的代码中,我添加了一些注释去除了一些无关紧要的代码,使用文字描述带过。不会对源码的理解造成影响。

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {


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

    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }

    // 如果url错误、或者下载options没有重试的选项且已经下载失败过一次的url 直接返回初始的operation抛出错误。
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

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

    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        // 磁盘缓存没有命中 || 刷新磁盘缓存 || 针对当前的url是否需要下载(默认YES,开发者可以根据代理配置为NO)开启下载任务
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                // 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.
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // 设置下载任务的options:如优先级、下载进度条是否显示等,参考SDWebImageOptions
            
            // 开启下载任务,返回下载任务的token用于取消操作
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 任务取消,不做处理
                } else if (error) {
                    // 下载失败,调用完成回调
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                
                    // 合理的错误将url加入失败的url数组。便于重试下载(合理是指:url对应的真实资源、非用户主动取消下载等)
                    if (error.code) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    // 从失败数组中移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } 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];
                                // 对图片缓存(比对处理后的图片是否有所改动。改动后不一致会重新对图片encodeData)
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            // 回调
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            // 直接对下载完成的图片缓存、
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        // 回调
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    // 从下载数组中移除当前完成的下载operation
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            
            
            operation.cancelBlock = ^{
                // 取消下载、移除下载中的记录
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        } else if (cachedImage) {
            // 缓冲命中 回调返回 移除下载记录
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            
            // 缓冲未命中、不允许下载直接回调返回 移除下载记录
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;

我们可以看到loadImageWithURL:方法中:

  • 首先根据url获取对应图片缓存的key值
  • self.imageCache开始查询缓存
  • 缓存命中根据options判断是否需要刷新缓存或者缓存未命中,开启下载任务。
  • self.failedURLs进行操作时都加上synchronized同步锁,防止多线程问题。

SDWebImage根据缓存是否命中决定是否下载图片(除了开发时指定需要刷新本地缓存)。

图片缓存

self.imageCache是SDWebImageManager管理的一个缓存单例。self.imageCache使用url作为查询的key值,在内存和磁盘上开始查询。首先会查询内存中是否存在图片。存在返回图片数据,不会再往下查询。不存在则再进行磁盘查询,查询磁盘缓存时以url为key经过MD5计算后拼接得到的完整磁盘路径后异步访问图片(读取磁盘内容)。缓冲如果命中后会首先进行内存缓冲,便于下次使用。最后都会调用done:的回调返回查找结果。


- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    // 读取内存(NSCache)中的图片
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if (image.images) {
            // 如果是动图,读取图片data
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }
    // 在内存中查询不到图片数据
    NSOperation *operation = [NSOperation new];
    //  耗时的io操作(磁盘查询)异步在ioQueue队列中
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // 判断读取操作是否被取消保护
            return;
        }
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            // 保存图片到内存中
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 回调
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    // 异步操作时返回operation,便于取消磁盘查找
    return operation;
}
图片下载

如果查询缓存未命中,SDWebImageManager就会使用self.imageDownloader进行网络图片的下载。即调用的downloadImageWithURL:options:progress:completed:。在downloadImageWithURL:options:progress:completed:
内部直接调用到了SDWebImageDownloader的addProgressCallback:completedBlock:forURL:createCallback:方法.主要的作用是在createCallback中会创建一个NSOperation的自定义子类SDWebImageDownloaderOperation以url为key存放到self.URLOperations字典中,同时对operation绑定其对应的下载进度回调(progressBlock)和完成下载的回调(completedBlock)并自动加入到下载队列(self.downloadQueue)中开启下载任务(self.downloadQueue:默认最大的maxConcurrentDownloads为6)。

每个SDWebImageDownloaderOperation的对象遵守了NSURLSession的各个代理方法。所以在下载过程中,在NSURLSession的代理方法上调用对应的progressBlock下载进度和最后下载完成时调用completedBlock。

createCallback:部分主要代码

SDWebImageDownloaderOperation(^createCallback)(void) =^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        // 设置下载图片的时间(默认15.0)
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 创建一个网络请求request,设置一系列属性:HTTPHeaders、cookie、缓存策略、是否等待相应返回等
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        // operation内部遵守了NSURLSession的代理方法
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        // 是否解压图片
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        // 设置url请求的验证
        
        // 设置下载的优先级
        
        // 加入队列开始下载任务
        [sself.downloadQueue addOperation:operation];
        // 设置任务之间的依赖
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }
        return operation;
    }];

注意operation.shouldDecompressImages设置为YES时,网络上有人碰到下载高清大图会有内存的问题。关于图片解压缩的问题可以看看这篇文章谈谈 iOS 中图片的解压缩

当然对每个未完成下载operation多次设置下载时,都会先用ur为用key对字典self.URLOperations进行取值。为nil才会调用createCallback();如果存在也只是重新绑定一次progressBlockcompletedBlock
注意downloadImageWithURL:options:progress:completed: 会返回一个SDWebImageDownloadToken的对象用来取消图片下载操作。SDWebImageDownloadToken组合了urlprogressBlockcompletedBlock回调。在需要取消的时候使用urlself.URLOperations取出对应的下载operation进行cancel、和取消对应的回调。

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {

    // 返回后续用来取消下载操作的组合token
    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
        // 读取是否正在下载
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            // 创建新的下载任务加入到URLOperations中
            operation = createCallback();
            self.URLOperations[url] = operation;
            // 设置完成回调,从URLOperations移除当前operation
            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation;
                    if (!soperation) return;
                    if (self.URLOperations[url] == soperation) {
                        [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        }
        // 将operation的回调progressBlock、completedBlock组合callbackBlocks。便于取消
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        // token组合url和downloadOperationCancelToken。
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

注意 源码中多处出现dispatch_barrier_asyncdispatch_barrier_sync的配套使用,主要是确保线程安全。

以上介绍了 SDWebImage主要的下载流程。但SDWebImage在图片下载的过程为我们过滤掉了很多的不利情况并且做了很多的优化和代码实现。这些都是如果我们不深入代码是不会了解到。比如磁盘缓存图片路径对key的MD5处理防止重名、图片的编码转换、图片的解压缩、图片缓存(NSCache)的清理、自定义NSOperation。
在下一篇中会介绍下SDWebImage中比较重要的技术点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,641评论 6 525
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,754评论 3 408
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 173,075评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,369评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,386评论 6 402
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,800评论 1 317
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,122评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,130评论 0 281
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,689评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,693评论 3 348
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,804评论 1 356
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,399评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,128评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,528评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,705评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,409评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,878评论 2 368