iOS源码解析—SDWebImage(SDWebImageManager)

概述

SDWebImageManager是SDWebImage框架的核心调度类,负责图片的缓存和下载逻辑,本文分析一下该类的相关方法。

初始化方法

首先SDWebImageManager维护类几个属性,类的定义注释如下:

@interface SDWebImageManager ()
@property (strong, nonatomic, readwrite) SDImageCache *imageCache; //缓存类
@property (strong, nonatomic, readwrite) SDWebImageDownloader *imageDownloader; //下载类
@property (strong, nonatomic) NSMutableSet *failedURLs; //下载失败的url
@property (strong, nonatomic) NSMutableArray *runningOperations; //当前执行的operation
@end

imageCache用于缓存图片数据的类,具体可以参考第一篇文章,imageDownloader用于下载图片数据,具体可以参考上一篇文章,failedURLs是一个数组,里面存储了获取图片失败对应的url,runningOperations存储了当前正在执行的operation,operation是SDWebImageCombinedOperation类型的对象,定义注释如下:

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock; //cancel block
@property (strong, nonatomic) NSOperation *cacheOperation; //执行缓存操作的operaton
@end

SDWebImageOperation是protocol,SDWebImageCombinedOperation实现该protocol,实现了cancel方法。注释如下;

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel]; //取消缓存operation的操作
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock(); //执行cancelBlock
        _cancelBlock = nil;
    }
}

然后SDWebImageManager通过-(void)initWithCache:downloader:方法初始化,注释如下:

- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache; //赋值cache
        _imageDownloader = downloader; //赋值downloader
        _failedURLs = [NSMutableSet new];
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

下载图片方法

SDWebImageManager中的核心方法是-(void)downloadImageWithURL:options:progress:completed:方法,主要负责从缓存中取图片,然后根据策略下载图片。改方法的步骤主要分为以下几步:

  1. 判断前置条件:

    if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
        //url判空
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
     //创建operation对象
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
        BOOL isFailedUrl = NO;
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
     //当前url是否是FailedUrl,如果是直接返回上层。
        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;
        }
    

    首先对url判空,如果不为空,判断当前url是否属于failedURLs中,因为failedURLs中的url属于之前下载失败的图片url,不能被下载。除非options中包含SDWebImageRetryFailed选项,表示之前下载失败过,但是本次需要重新下载。如果不包含SDWebImageRetryFailed选项,调用completedBlock回调。

  2. 将operation加入runningOperations中,从缓存中去图片数据:

    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
     if (operation.isCancelled) {
             @synchronized (self.runningOperations) {
             [self.runningOperations removeObject:operation];
         }
             return;
        }
         //不存在image,或者需要刷新image,重新下载
     if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
             ...
     }
         else if (image) { //存在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 {
         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];
         }
     }
    }]
    

    该方法首先从缓存中取image,如果没有取到,说明是第一次下载图片,或者存在image,但是需要刷新图片,如果delegate实现shouldDownloadImageForURL方法,返回YES,说明需要下载,这些情况下开始下载图片。当不属于这些情况且image存在时,说明之前已经下载过url对应的图片了,则直接将缓存中的图片通过completedBlock返回,并且将operation从runningOperations中删除。如果image不存在,则返回nil给外部。

  3. 当需要下载图片时,设置标记位,将SDWebImageOptions转化成SDWebImageDownloaderOptions:

    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;
    if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
    if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
    if (image && options & SDWebImageRefreshCached) {
     //禁用ProgressiveDownload
     downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
         //开启忽略CachedResponse
     downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
    }
    
  4. 然后调用imageDownloader下载url对应图片数据,并处理不同情况:

    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) {
     }
     else if (error) { //下载失败
         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) {
                 @synchronized (self.failedURLs) {
                     [self.failedURLs addObject:url]; //添加url进failedURLs
                     }
                 }
         }
     else {
         if ((options & SDWebImageRetryFailed)) {
             @synchronized (self.failedURLs) {
                 [self.failedURLs removeObject:url]; //下载成功,将url从failedURLs中删除
             }
         }
             BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
         if (options & SDWebImageRefreshCached && image && !downloadedImage) {
         }
            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];
                     [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);
                    }
                });
            });
            }
             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) {
                     //从runningOperations中删除
                     [self.runningOperations removeObject:strongOperation];
                }
            }
        }
    }];
    operation.cancelBlock = ^{
     [subOperation cancel]; //取消下载
     @synchronized (self.runningOperations) {
             __strong __typeof(weakOperation) strongOperation = weakOperation;
         if (strongOperation) { //从runningOperations中删除
             [self.runningOperations removeObject:strongOperation];
         }
     }
    };
    

    如果下载失败,则返回nil给外层,同时判断errorCode不是超时、取消、找不到主机地址等错误时,将url加入failedURLs。如果下载成功,且option标记为SDWebImageRetryFailed,则键url从failedURLs数组中删除。然后判断是否需要将下载的图片进行变换,如果需要transform,则进行transform后将图片写入缓存,否则直接将原图片写入缓存。当操作执行完后,将operation从runningOperations中删除。最后指定了operation的cancelBlock,该block主要讲负责下载的subOperation取消,然后将当前operation从runningOperations数组中删除。

其他方法

SDWebImageManager还提供了一些辅助方法,如下:

  1. -(void)saveImageToCache: forURL:方法,该方法负责将图片存入缓存,url作为key。

    - (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url {
        if (image && url) {
            NSString *key = [self cacheKeyForURL:url];
            [self.imageCache storeImage:image forKey:key toDisk:YES];
        }
    }
    
  2. -(void)cancelAll方法,该方法将runningOperations中的所有operation取消。

    - (void)cancelAll {
        @synchronized (self.runningOperations) {
            NSArray *copiedOperations = [self.runningOperations copy];
            [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; //取消
            [self.runningOperations removeObjectsInArray:copiedOperations];
        }
    }
    

    调用makeObjectsPerformSelector方法遍历operation,调用cancel方法,SDWebImageCombinedOperation的cancel方法在上文有注释,取消cacheOperation,执行cancelBlock取消下载的operation。

  3. -(BOOL)cachedImageExistsForURL:方法和-(BOOL)diskImageExistsForURL:方法

    这两个同步方法返回图片是否在缓存中存在,前一个方法先判断图片是否在内存中存在,如果没有再去文件中查找,返回查找的结果。后一个方法直接去文件中查找,返回查找的结果。

  4. -(void)cachedImageExistsForURL:completion:方法和-(void)diskImageExistsForURL:completion:方法

    这两个方法的功能和上面两个是相同的,前一个方法同时从内存和文件缓存中查找图片是否存在,后一个方法只从文件缓存中查找,区别之处在于,这两个方法是通过completionBlock异步返回结果的。

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

推荐阅读更多精彩内容