优秀开源库SDWebImage源码浅析

世人都说阅读源代码对于功力的提高是十分显著的, 可是不少的著名开源框架源代码动辄上万行, 复杂度实在过高, 这里只作基础的分析。git

简洁的接口

首先来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要做用就是:github

Asynchronous image downloader with cache support with an UIImageView category.web

一个异步下载图片而且支持缓存的 UIImageView 分类.缓存

就这么直译过来相信各位也能理解, 框架中最最经常使用的方法其实就是这个:bash

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
                  
复制代码

固然这个框架中还有 UIButton 的分类, 能够给 UIButton 异步加载图片, 不过这个并无 UIImageView 分类中的这个方法经常使用.网络

这个框架的设计仍是极其的优雅和简洁, 主要的功能就是这么一行代码, 而其中复杂的实现细节所有隐藏在这行代码以后, 正应了那句话:框架

把简洁留给别人, 把复杂留给本身.异步

咱们已经看到了这个框架简洁的接口, 接下来咱们看一下 SDWebImage 是用什么样的方式优雅地实现异步加载图片和缓存的功能呢?async

复杂的实现

其实复杂只是相对于简洁而言的, 并非说 SDWebImage 的实现就很糟糕, 相反, 它的实现仍是很是 amazing 的, 在这里咱们会忽略不少的实现细节, 并不会对每一行源代码逐一解读.post

首先, 咱们从一个很高的层次来看一下这个框架是如何组织的.

UIImageView+WebCacheUIButton+WebCache 直接为表层的 UIKit 框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloaderSDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.

UIImageView+WebCache

接下来咱们就以 UIImageView+WebCache 中的

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder;
复制代码

这一方法为入口研究一下 SDWebImage 是怎样工做的. 咱们打开上面这段方法的实现代码 UIImageView+WebCache.m

固然你也能够 git clone git@github.com:rs/SDWebImage.git 到本地来查看.

- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder {
    [self sd_setImageWithURL:url
            placeholderImage:placeholder
                     options:0
                    progress:nil
                   completed:nil];
}
复制代码

这段方法惟一的做用就是调用了另外一个方法

[self sd_setImageWithURL:placeholderImage:options:progress:completed:]
复制代码

在这个文件中, 你会看到不少的 sd_setImageWithURL...... 方法, 它们最终都会调用上面这个方法, 只是根据须要传入不一样的参数, 这在不少的开源项目中乃至咱们平时写的项目中都是很常见的. 而这个方法也是 UIImageView+WebCache 中的核心方法.

这里就再也不复制出这个方法的所有实现了.

操做的管理

这是这个方法的第一行代码:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #1

[self sd_cancelCurrentImageLoad];
复制代码

这行看似简单的代码最开始是被我忽略的, 我后来才发现蕴藏在这行代码以后的思想, 也就是 SDWebImage 管理操做的办法.

框架中的全部操做实际上都是经过一个 operationDictionary 来管理, 而这个字典其实是动态的添加到 UIView 上的一个属性, 至于为何添加到 UIView 上, 主要是由于这个 operationDictionary 须要在 UIButtonUIImageView 上重用, 因此须要添加到它们的根类上.

这行代码是要保证没有当前正在进行的异步下载操做, 不会与即将进行的操做发生冲突, 它会调用:

// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1

[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]
复制代码

而这个方法会使当前 UIImageView 中的全部操做都被 cancel. 不会影响以后进行的下载操做.

占位图的实现

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #4

if (!(options & SDWebImageDelayPlaceholder)) {
    self.image = placeholder;
}
复制代码

若是传入的 options 中没有 SDWebImageDelayPlaceholder(默认状况下 options == 0), 那么就会为 UIImageView 添加一个临时的 image, 也就是占位图.

获取图片

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8

if (url)
复制代码

接下来会检测传入的 url 是否非空, 若是非空那么一个全局的 SDWebImageManager 就会调用如下的方法获取图片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
复制代码

下载完成后会调用 (SDWebImageCompletionWithFinishedBlock)completedBlockUIImageView.image 赋值, 添加上最终所须要的图片.

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #10

dispatch_main_sync_safe(^{
    if (!wself) return;
    if (image) {
        wself.image = image;
        [wself setNeedsLayout];
    } else {
        if ((options & SDWebImageDelayPlaceholder)) {
            wself.image = placeholder;
            [wself setNeedsLayout];
        }
    }
    if (completedBlock && finished) {
        completedBlock(image, error, cacheType, url);
    }
});
复制代码

dispatch_main_sync_safe 宏定义

上述代码中的 dispatch_main_sync_safe 是一个宏定义, 点进去一看发现宏是这样定义的

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }
复制代码

相信这个宏的名字已经讲他的做用解释的很清楚了: 由于图像的绘制只能在主线程完成, 因此, dispatch_main_sync_safe 就是为了保证 block 能在主线程中执行.

而最后, 在 [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:] 返回 operation 的同时, 也会向 operationDictionary 中添加一个键值对, 来表示操做的正在进行:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #28

[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
复制代码

它将 opertion存储到 operationDictionary 中方便之后的 cancel.

到此为止咱们已经对 SDWebImage 框架中的这一方法分析完了, 接下来咱们将要分析 SDWebImageManager 中的方法

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
复制代码

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: 来直接下载图片.

能够看到, 这个类的主要做用就是为 UIImageView+WebCacheSDWebImageDownloader, SDImageCache 之间构建一个桥梁, 使它们可以更好的协同工做, 咱们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #6

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}
复制代码

这块代码的功能是肯定 url 是否被正确传入, 若是传入参数的是 NSString 类型就会被转换为 NSURL. 若是转换失败, 那么 url 会被赋值为空, 这个下载的操做就会出错.

SDWebImageCombinedOperation

url 被正确传入以后, 会实例一个很是奇怪的 “operation”, 它实际上是一个遵循 SDWebImageOperation 协议的 NSObject 的子类. 而这个协议也很是的简单:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

复制代码

这里仅仅是将这个 SDWebImageOperation 类包装成一个看着像 NSOperation 其实并非 NSOperation 的类, 而这个类惟一与 NSOperation 的相同之处就是它们均可以响应 cancel 方法. (不知道这句看似像绕口令的话, 你看懂没有, 若是没看懂..请多读几遍).

而调用这个类的存在实际是为了使代码更加的简洁, 由于调用这个类的 cancel 方法, 会使得它持有的两个 operation 都被 cancel.

// SDWebImageCombinedOperation
// cancel #1

- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        _cancelBlock = nil;
    }
}
复制代码

而这个类, 应该是为了实现更简洁的 cancel 操做而设计出来的.

既然咱们获取了 url, 再经过 url 获取对应的 key

NSString *key = [self cacheKeyForURL:url]; 下一步是使用 key 在缓存中查找之前是否下载过相同的图片.

operation.cacheOperation = [self.imageCache
		queryDiskCacheForKey:key
        			    done:^(UIImage *image, SDImageCacheType cacheType) { ... }];
        			    
复制代码

这里调用 SDImageCache 的实例方法 queryDiskCacheForKey:done: 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 NSOperation.

若是咱们在缓存中查找到了对应的图片, 那么咱们直接调用 completedBlock 回调块结束这一次的图片下载操做.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47

dispatch_main_sync_safe(^{
    completedBlock(image, nil, cacheType, YES, url);
});
复制代码

若是咱们没有找到图片, 那么就会调用 SDWebImageDownloader 的实例方法:

id <SDWebImageOperation> subOperation =
  [self.imageDownloader downloadImageWithURL:url
                                     options:downloaderOptions
                                    progress:progressBlock
                                   completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];
                                   
复制代码

若是这个方法返回了正确的 downloadedImage, 那么咱们就会在全局的缓存中存储这个图片的数据:

[self.imageCache storeImage:downloadedImage
	   recalculateFromImage:NO
                  imageData:data
                     forKey:key
                     toDisk:cacheOnDisk];
                     
复制代码

并调用 completedBlockUIImageView 或者 UIButton 添加图片, 或者进行其它的操做.

最后, 咱们将这个 subOperationcancel 操做添加到 operation.cancelBlock 中. 方便操做的取消.

operation.cancelBlock = ^{
    [subOperation cancel];
    }
复制代码

SDWebImageCache

SDWebImageCache.h 这个类在源代码中有这样的注释:

SDImageCache maintains a memory cache and an optional disk cache.

它维护了一个内存缓存和一个可选的磁盘缓存, 咱们先来看一下在上一阶段中没有解读的两个方法, 首先是:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key
                                 done:(SDWebImageQueryCompletedBlock)doneBlock;
复制代码

这个方法的主要功能是异步的查询图片缓存. 由于图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.

// SDWebImageCache
// queryDiskCacheForKey:done: #9

UIImage *image = [self imageFromMemoryCacheForKey:key];
复制代码

这个 imageFromMemoryCacheForKey 方法会在 SDWebImageCache 维护的缓存 memCache 中查找是否有对应的数据, 而 memCache 就是一个 NSCache.

若是在内存中并无找到图片的缓存的话, 就须要在磁盘中寻找了, 这个就比较麻烦了..

在这里会调用一个方法 diskImageForKey 这个方法的具体实现我在这里就不介绍了, 涉及到不少底层 Core Foundation 框架的知识, 不过这里文件名字的存储使用 MD5 处理事后的文件名.

// SDImageCache
// cachedFileNameForKey: #6

CC_MD5(str, (CC_LONG)strlen(str), r);
复制代码

对于其它的实现细节也就很少说了…

若是在磁盘中查找到对应的图片, 咱们会将它复制到内存中, 以便下次的使用.

// SDImageCache
// queryDiskCacheForKey:done: #24

UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
    CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale;
    [self.memCache setObject:diskImage forKey:key cost:cost];
}
复制代码

这些就是 SDImageCache 的核心内容了, 而接下来将介绍若是缓存没有命中, 图片是如何被下载的.

SDWebImageDownloader

按照以前的惯例, 咱们先来看一下 SDWebImageDownloader.h 中对这个类的描述.

Asynchronous downloader dedicated and optimized for image loading.

专用的而且优化的图片异步下载器.

这个类的核心功能就是下载图片, 而核心方法就是上面提到的:

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

回调

这个方法直接调用了另外一个关键的方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                     forURL:(NSURL *)url
             createCallback:(SDWebImageNoParamsBlock)createCallback
复制代码

它为这个下载的操做添加回调的块, 在下载进行时, 或者在下载结束时执行一些操做, 先来阅读一下这个方法的源代码:

// SDWebImageDownloader
// addProgressCallback:andCompletedBlock:forURL:createCallback: #10

BOOL first = NO;
if (!self.URLCallbacks[url]) {
    self.URLCallbacks[url] = [NSMutableArray new];
    first = YES;
}

// Handle single download of simultaneous download request for the same URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

if (first) {
    createCallback();
}
复制代码

方法会先查看这个 url 是否有对应的 callback, 使用的是 downloader 持有的一个字典 URLCallbacks.

若是是第一次添加回调的话, 就会执行 first = YES, 这个赋值很是的关键, 由于 first 不为 YES 那么 HTTP 请求就不会被初始化, 图片也没法被获取.

而后, 在这个方法中会从新修正在 URLCallbacks 中存储的回调块.

NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
复制代码

若是是第一次添加回调块, 那么就会直接运行这个 createCallback 这个 block, 而这个 block, 就是咱们在前一个方法 downloadImageWithURL:options:progress:completed: 中传入的回调块.

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #4

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];
复制代码

咱们下面来分析这个传入的无参数的代码. 首先这段代码初始化了一个 NSMutableURLRequest:

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #11

NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
		initWithURL:url
        cachePolicy:...
    timeoutInterval:timeoutInterval];
复制代码

这个 request 就用于在以后发送 HTTP 请求.

在初始化了这个 request 以后, 又初始化了一个 SDWebImageDownloaderOperation 的实例, 这个实例, 就是用于请求网络资源的操做. 它是一个 NSOperation 的子类,

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #20

operation = [[SDWebImageDownloaderOperation alloc]
		initWithRequest:request
                options:options
               progress:...
              completed:...
              cancelled:...}];
              
复制代码

可是在初始化以后, 这个操做并不会开始(NSOperation 实例,只有在调用 start 方法或者加入 NSOperationQueue 才会执行), 咱们须要将这个操做加入到一个 NSOperationQueue 中.

// SDWebImageDownloader
// downloadImageWithURL:option:progress:completed: #59

[wself.downloadQueue addOperation:operation];
复制代码

只有将它加入到这个下载队列中, 这个操做才会执行.

SDWebImageDownloaderOperation

这个类就是处理 HTTP 请求, URL 链接的类, 当这个类的实例被加入队列以后, start 方法就会被调用, 而 start 方法首先就会产生一个 NSURLConnection.

// SDWebImageDownloaderOperation
// start #1

@synchronized (self) {
    if (self.isCancelled) {
        self.finished = YES;
        [self reset];
        return;
    }
    self.executing = YES;
    self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
    self.thread = [NSThread currentThread];
}
复制代码

而接下来这个 connection 就会开始运行:

// SDWebImageDownloaderOperation
// start #29

[self.connection start];
复制代码

它会发出一个 SDWebImageDownloadStartNotification 通知

// SDWebImageDownloaderOperation
// start #35

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
复制代码

代理

start 方法调用以后, 就是 NSURLConnectionDataDelegate中代理方法的调用.

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;
复制代码

在这三个代理方法中的前两个会不停回调 progressBlock 来提示下载的进度.

而最后一个代理方法会在图片下载完成以后调用 completionBlock 来完成最后 UIImageView.image 的更新.

而这里调用的 progressBlock completionBlock cancelBlock 都是在以前存储在 URLCallbacks 字典中的.

到目前为止, 咱们就基本解析了 SDWebImage

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
复制代码

这个方法执行的所有过程了.

总结

SDWebImage 的图片加载过程其实很符合咱们的直觉:

查看缓存 缓存命中 * 返回图片 更新 UIImageView 缓存未命中 * 异步下载图片 加入缓存 更新 UIImageView

相关文章
相关标签/搜索