mjbaishiyun 2019-06-30
世人都说阅读源代码对于功力的提升是十分显著的, 但是很多的著名开源框架源代码动辄上万行, 复杂度实在太高, 这里只做基础的分析。
首先来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:
Asynchronous image downloader with cache support with an UIImageView category.一个异步下载图片并且支持缓存的 UIImageView 分类.
就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];当然这个框架中还有 UIButton 的分类, 可以给 UIButton 异步加载图片, 不过这个并没有 UIImageView 分类中的这个方法常用.
这个框架的设计还是极其的优雅和简洁, 主要的功能就是这么一行代码, 而其中复杂的实现细节全部隐藏在这行代码之后, 正应了那句话:
把简洁留给别人, 把复杂留给自己.我们已经看到了这个框架简洁的接口, 接下来我们看一下 SDWebImage 是用什么样的方式优雅地实现异步加载图片和缓存的功能呢?
其实复杂只是相对于简洁而言的, 并不是说 SDWebImage 的实现就很糟糕, 相反, 它的实现还是非常 amazing 的, 在这里我们会忽略很多的实现细节, 并不会对每一行源代码逐一解读.
首先, 我们从一个很高的层次来看一下这个框架是如何组织的.
UIImageView+WebCache 和 UIButton+WebCache 直接为表层的 UIKit 框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloader 和 SDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.
接下来我们就以 UIImageView+WebCache 中的
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder;这一方法为入口研究一下 SDWebImage 是怎样工作的. 我们打开上面这段方法的实现代码 UIImageView+WebCache.m
当然你也可以 git clone [email protected]: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 需要在 UIButton 和 UIImageView 上重用, 所以需要添加到它们的根类上.
这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:
// 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)completedBlock 为 UIImageView.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 是一个宏定义, 点进去一看发现宏是这样定义的
#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.h 中你可以看到关于 SDWebImageManager 的描述:
这个类就是隐藏在 UIImageView+WebCache 背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 来直接下载图片.
可以看到, 这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader, 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 会被赋值为空, 这个下载的操作就会出错.
当 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];并调用 completedBlock 对 UIImageView 或者 UIButton 添加图片, 或者进行其它的操作.
最后, 我们将这个 subOperation 的 cancel 操作添加到 operation.cancelBlock 中. 方便操作的取消.
operation.cancelBlock = ^{
[subOperation cancel];
}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.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];
只有将它加入到这个下载队列中, 这个操作才会执行.
这个类就是处理 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