SDWebImage源码解析(一)

SDWebImage是一个图片下载的开源项目,由于它提供了简介的接口以及异步下载与缓存的强大功能,深受“猿媛“的喜爱。截止到本篇文章开始,项目的star数已经超过1.6k了。今天我就对项目的源码做个阅读笔记,一方面归纳总结自己的心得,另一方面给准备阅读源码的童鞋做点铺垫工作。代码最新版本为3.8。

正如项目的第一句介绍一样:

Asynchronous image downloader with cache support as a UIImageView category

SDWebImage是个支持异步下载与缓存的UIImageView扩展。项目主要提供了一下功能:

  • 扩展UIImageView, UIButton, MKAnnotationView,增加网络图片与缓存管理。
  • 一个异步的图片加载器
  • 一个异步的 内存 + 磁盘 图片缓存,拥有自动的缓存过期处理机制。
  • 支持后台图片解压缩处理
  • 确保同一个 URL 的图片不被多次下载
  • 确保虚假的 URL 不会被反复加载
  • 确保下载及缓存时,主线程不被阻塞
  • 使用 GCD 与 ARC

项目支持的图片格式包括PNG,JEPG,GIF,WebP等等。

先看看SDWebImage的项目组织架构:

SDWebImage组织架构.png

SDWebImageDownloader负责维持图片的下载队列;
SDWebImageDownloaderOperation负责真正的图片下载请求;
SDImageCache负责图片的缓存;
SDWebImageManager是总的管理类,维护了一个SDWebImageDownloader实例和一个SDImageCache实例,是下载与缓存的桥梁;
SDWebImageDecoder负责图片的解压缩;
SDWebImagePrefetcher负责图片的预取;
UIImageView+WebCache和其他的扩展都是与用户直接打交道的。

其中,最重要的三个类就是SDWebImageDownloaderSDImageCacheSDWebImageManager。接下来我们就分别详细地研究一下这些类各自具体做了哪些事,又是怎么做的。

为了便于大家从宏观上有个把握,我这里先给出项目的框架结构:

Paste_Image.png

UIImageView+WebCacheUIButton+WebCache直接为表层的 UIKit框架提供接口, 而 SDWebImageManger负责处理和协调SDWebImageDownloaderSDWebImageCache, 并与 UIKit层进行交互。SDWebImageDownloaderOperation真正执行下载请求;最底层的两个类为高层抽象提供支持。
我们按照从上到下执行的流程来研究各个类

UIImageView+WebCache

这里,我们只用UIImageView+WebCache来举个例子,其他的扩展类似。
常用的场景是已知图片的url地址,来下载图片并设置到UIImageView上。UIImageView+WebCache提供了一系列的接口:

- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock;

这些接口最终会调用

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

方法的第一行代码[self sd_cancelCurrentImageLoad]是取消UIImageView上当前正在进行的异步下载,确保每个 UIImageView 对象中永远只存在一个 operation,当前只允许一个图片网络请求,该 operation 负责从缓存中获取 image 或者是重新下载 image。具体执行代码是:

// UIView+WebCacheOperation.m
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self 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];
}

实际上,所有的操作都是由一个operationDictionary字典维护的,执行新的操作之前,先cancel所有的operation。这里的cancel是SDWebImageOperation协议里面定义的。

//预览 占位图
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

是一种占位图策略,作为图片下载完成之前的替代图片。dispatch_main_async_safe是一个宏,保证在主线程安全执行,最后再讲。
然后判断url,url为空就直接调用完成回调,报告错误信息;否则,用SDWebImageManager单例的

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

方法下载图片。下载完成之后刷新UIImageView的图片。

//图像的绘制只能在主线程完成
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 {
            //image== nil,设置占位图
            if ((options & SDWebImageDelayPlaceholder)) {
                wself.image = placeholder;
                [wself setNeedsLayout];
            }
    }
    if (completedBlock && finished) {
        completedBlock(image, error, cacheType, url);
    }
});

最后,把返回的id <SDWebImageOperation> operation添加到operationDictionary中,方便后续的cancel。

SDWebImageManager

SDWebImageManager.h中是这样描述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.

即隐藏在UIImageView+WebCache背后,用于处理异步下载和图片缓存的类,当然你也可以直接使用 SDWebImageManager 的方法 downloadImageWithURL:options:progress:completed:来直接下载图片。

SDWebImageManager.h首先定义了一些枚举类型的SDWebImageOptions。关于这些Options的具体含义可以参考叶孤城大神的解析

然后,声明了三个block:

//操作完成的回调,被上层的扩展调用。
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
//被SDWebImageManager调用。如果使用了SDWebImageProgressiveDownload标记,这个block可能会被重复调用,直到图片完全下载结束,finished=true,再最后调用一次这个block。
typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
//SDWebImageManager每次把URL转换为cache key的时候调用,可以删除一些image URL中的动态部分。
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

定义了SDWebImageManagerDelegate协议:

@protocol SDWebImageManagerDelegate <NSObject>

@optional

/**
 * Controls which image should be downloaded when the image is not found in the cache.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param imageURL     The url of the image to be downloaded
 *
 * @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
 * 控制在cache中没有找到image时 是否应该去下载。
 */
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

/**
 * Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
 * NOTE: This method is called from a global queue in order to not to block the main thread.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param image        The image to transform
 * @param imageURL     The url of the image to transform
 *
 * @return The transformed image object.
 * 在下载之后,缓存之前转换图片。在全局队列中操作,不阻塞主线程
 */
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

SDWebImageManager是单例使用的,分别维护了一个SDImageCache实例和一个SDWebImageDownloader实例。 类方法分别是:

//初始化SDWebImageManager单例,在init方法中已经初始化了cache单例和downloader单例。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
//下载图片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//缓存给定URL的图片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消当前所有的操作
- (void)cancelAll;
//监测当前是否有进行中的操作
- (BOOL)isRunning;
//监测图片是否在缓存中, 先在memory cache里面找  再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
//监测图片是否缓存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
//监测图片是否在缓存中,监测结束后调用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//监测图片是否缓存在disk里,监测结束后调用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回给定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;

我们主要研究

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

首先,监测url 的合法性:

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

第一个判断条件是防止很多用户直接传递NSString作为NSURL导致的错误,第二个判断条件防止crash。

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

集合failedURLs保存之前失败的urls,如果url为空或者url之前失败过且不采用重试策略,直接调用completedBlock返回错误。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

runningOperations是一个可变数组,保存所有的operation,主要用来监测是否有operation在执行,即判断running 状态。

SDWebImageManager会首先在memory以及disk的cache中查找是否下载过相同的照片,即调用imageCache

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock 

方法。
如果在缓存中找到图片,直接调用completedBlock,第一个参数是缓存的image。

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !strongOperation.isCancelled) {//为啥这里用strongOperation TODO
        completedBlock(image, nil, cacheType, YES, url);
    }
});

如果没有在缓存中找到图片,或者不管是否找到图片,只要operation有SDWebImageRefreshCached标记,那么若SDWebImageManagerDelegateshouldDownloadImageForURL方法返回true,即允许下载时,都使用 imageDownloader

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

方法进行下载。如果下载有错误,直接调用completedBlock返回错误,并且视情况将url添加到failedURLs里面;

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从failURLs里删除:

if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
         [self.failedURLs removeObject:url];
    }
}

如果delegate实现了,imageManager:transformDownloadedImage:withURL:方法,图片在缓存之前,需要做转换(在全局队列中调用,不阻塞主线程)。转化成功切下载全部结束,图片存入缓存,调用completedBlock回调,第一个参数是转换后的image。

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

否则,直接存入缓存,调用completedBlock回调,第一个参数是下载的原始image。

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

存入缓存都是调用imageCache

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk

方法。

如果没有在缓存找到图片,且不允许下载,直接调用completedBlock,第一个参数为nil。

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !weakOperation.isCancelled) {//为啥这里用weakOperation TODO
        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
    }
});

最后都要将这个operation从runningOperations里删除。

@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
 }

这里再说一下上面的operation,是一个SDWebImageCombinedOperation实例:

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;

@end

是一个遵循SDWebImageOperation协议的NSObject子类。

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

在里面封装一个NSOperation,这么做的目的应该是为了使代码更简洁。因为下载操作需要查询缓存的operation和实际下载的operation,这个类的cancel方法可以同时cancel两个operation,同时还可以维护一个状态cancelled。
敬请期待后续更新!

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

推荐阅读更多精彩内容