SDWebImage源码分析 2

学习源码时可能太过枯燥,一个不错背景音乐能让心情平静提升专注力(๑•̀ㅂ•́)و✧
推荐歌单:http://music.163.com/#/m/playlist?id=6683129

接着上回,我们还留着downloadImageWithURL没说,现在就到SDWebImageManager.h里来看看:

/**
 * 如果图片的url不在缓存中则下载,否则使用缓存中的图片.
 *
 * @param url            图片的url
 * @param options        选项
 * @param progressBlock  当图片正在下载中调用该block
 * @param completedBlock 当任务完成后调用该block.
 *
 *   completedBlock必须提供,不允许为nil.
 *   typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
 *   这个block没有返回值 并且 将请求的UIImage作为第一个参数.
 *   如果发生错误,image参数将会为nil,第二个参数会包含NSError
 *
 *   第三个参数是`SDImageCacheType`枚举用于表示图片是从本地缓存(磁盘)还是内存或是网络中获取的
 *   
 *   当开启了SDWebImageProgressiveDownload选项,并且正在下载图片时,finished会被设置成NO。当图片被完整的下载好后会执行block传入完整的图片以及将之设置为YES
 *
 * @return 返回一个遵循SDWebImageOperation协议的NSObject. 应该是一个SDWebImageDownloaderOperation的实例
 *
 */
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

从描述中可以了解到该方法会帮我们下载图片以及从缓存中取出以前下载过的图片。

同时我们注意到返回参数是id <SDWebImageOperation>,跟到SDWebImageOperation.h中看到里面仅仅只有一个方法:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

这个是面向协议的做法,想使用该接口的话要遵循接口中提供的方法来用。同时继承了该接口的子类需要对接口的方法实现。

SDWebImageManager.m中,我们将downloadImageWithURL 代码拆成几个片段来分析:

// completedBlock 必须要有
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//有个常见的错误,那就是将NSString传进url中,然而因为奇怪的原因xcode又不会发出警告。所以这里遇到这种情况会做一次转换
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// 防止传入错误的非NSURL类型造成闪退,这里处理了一下
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

这里主要是针对常见的错误进行处理,曾经我也将NSString误传进行调用,Xcode还没报错代码也能用,直到我翻开代码一看原来SDWebImage帮我们做了一层转换啊。

继续往下看发现代码中使用到了__block__weak修饰符:

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

__block的作用是在block内想要修改外部一个变量就需要在外头这个变量前加上该修饰符
__weak的作用是避免在block内出现循环引用

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

//如果url长度为0或者是未设置SDWebImageRetryFailed以及加载失败的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;
}

这段代码大意是:如果未设置SDWebImageRetryFailed进行下载失败重试的话,遇到进了黑名单的url就直接返回错误,因为不需要重试了嘛。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation]; //<往runningOperations加入当前的operation
}

这段代码中使用到了@synchronized用来保证线程安全,即仅只允许一个线程对runningOperations进行操作,其他线程阻塞。

NSString *key = [self cacheKeyForURL:url]; //将url换算成缓存的key

从名称上可以直接猜到[self cacheKeyForURL:url]方法的用意是为url创建一个缓存的key,跟进查看我们的猜测对不对:

- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}

这个方法简单明了将url转换成key,在用户未设置自定义规则(self.cacheKeyFilter)的时候直接返回url的absoluteString。

接下来轮到queryDiskCacheForKey这个方法了:

// 从缓存中查询image,先从内存找,找不到再到磁盘中找
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
      ...

我们跟到SDImageCache.m里查看:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) { //< 没有doneBlock直接返回nil
        return nil;
    }

    if (!key) { //< 没有key则调用doneBlock
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 先检查内存中是否有缓存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            //从磁盘中查找是否命中缓存
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) { //< 找到图片以及允许将图片缓存在内存中
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

大致可以从代码中看出些端倪,先使用key从内存中查找图片,找不到的话再去磁盘中查找,然后放到内存中并返回图片以及SDImageCacheType:

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * 该图片还未存入SDWebImage caches(SDWebImage 缓存)中,来自web上下载的。
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     */
    SDImageCacheTypeNone,
    /**
     * 该图片是从磁盘中获取的
     */
    SDImageCacheTypeDisk,
    /**
     * 该图片是从缓存中获取的
     */
    SDImageCacheTypeMemory
};

目前我们只需要了解该方法的作用是用来从缓存中取出图片用的就够了,具体的分析等到后面的章节再说。

我们的重点是接下来的代码,按照惯例将它们拆开来分析:

if (operation.isCancelled) { //< 当前的任务被取消
  @synchronized (self.runningOperations) {
      [self.runningOperations removeObject:operation]; //< 从runningOperations中删除掉
  }
  return;
}

这段代码意图明显,运行至此发现当前的operation已经被取消,那也不用继续执行了,将operation从运行任务列表中删除即可。

接下来进入分支条件,一条是需要从网络下载图片,一条是直接使用缓存,最后一条是无法找到图片且又设置了不允许下载。

if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { //< 如果缓存中找不到图片或者开启了SDWebImageRefreshCached(2选1) 并且 没实现shouldDownloadImageForURL

我们从第一条分支看起:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        //如果图片在缓存中被找到,但启用了SDWebImageRefreshCached就需要重新从服务器上下载图片使NSURLCache刷新缓存
        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;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (image && options & SDWebImageRefreshCached) { //< 开启了SDWebImageRefreshCached
    //强制关闭ProgressiveDownload
    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
    //忽略从NSURLCache缓存中读取
    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 将下载的事情交给SDWebImageDownloader来搞定,SDWebImageManager负责管理和调度,实现职责单一
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {

这里主要完成了SDWebImageManager的options到SDWebImageDownloader的options转换,将下载的事情交给SDWebImageDownloader来搞定。

接下来就是对异常的处理:

__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) { //< strongOperation被释放或者已经被取消了
    // 什么都不做
    // 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) { //< 有错误
  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];
      }
  }
}

这里将url加入黑名单,如未设置下载重试的话,下次请求该图片地址将直接略过

最后是当strongOperation存在且没有错误的情况:

//如果开启了SDWebImageRetryFailed将url从failedURLs中删除
if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
        [self.failedURLs removeObject:url];
    }
}
//如果开启SDWebImageCacheMemoryOnly,则cacheOnDisk为NO,即不缓存在磁盘上
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

if (options & SDWebImageRefreshCached && image && !downloadedImage) {
    //图片命中NSURLCache的缓存,则不调用completion block
}
// 如果图片(downloadedImage)下载成功并且想transform图片的情况
// 默认情况下,animated image是不允许transform的,得先使用downloadedImage.images判断这个图片是不是animated image,为nil即为静态图
// 但如果打开了SDWebImageTransformAnimatedImage,允许强制transform
// 要transform还需实现transformDownloadedImage这个delegate
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);
        }
    });
}

这段代码大意是图片下载完成,用户可以对图片进行处理,然后SDWebImage将它存入缓存中。

第二个分支是从缓存中找到了图片,非常简洁:

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];
}

直接调用completedBlock返回,然后再从runningOperations删除

第三条分支为图片不在缓存中并且在delegate中又不允许下载,所以image为nil,error也为nil:

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];
}

到此为止SDWebImageManager中关键的downloadImageWithURL代码的主流程我们已经大致理解了。

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

推荐阅读更多精彩内容