前言
SDWebImage
作为一个加载图片开源库,在大多数项目里它和一些刷新、弹窗控件一样普及。而且这个东西写得很好,网上已经有很多篇对此库剖析的文章了。正好这段时间有时间,我也花了好多天时间认真阅读了下它的代码。确实感觉受益颇多,因此就专门写篇笔记,一来再次梳理下思路,加深理解。二来也是为方便日后温习,温故而知新。
好了,开始。
分析
我自己画了一张图,展示了SDWebImage
在加载图片时的整个脉络结构。先将此图亮出,下面边分析边回过头来对照该图加深理解。
我们一般在在项目中是这样使用它的:
UIImage *image = [UIImage imageNamed:@"1.jpg"];
NSString *urlStr = @"http://dn-edu-test.qbox.me/5436557847300726784";
NSURL *url = [NSURL URLWithString:urlStr];
[_imgV setImageWithURL:url placeholderImage:image];
SDWebImage
提供的接口方法非常简单,只需要一个图片的url和一个占位图作为参数而已。而且它的接口方法是以UIImageView
的分类的方式提供的,而非通过继承的方式,这样降低了耦合和侵入性,同时也降低了使用者的使用成本,不用在创建UIImageView
时继承某类,或使用它封装好的某类。只要在类中导入了该分类的头文件,就可以通过UIImageView
的实例对象调用库里接口方法。
UIImageView+WebCache.m
我们从使用者的视角开始,从外向内一层层的分析它的代码。首先我们进入摁住command
键的同时点击[_imgV setImageWithURL:url placeholderImage:image];
,便进入了UIImageView+WebCache.m
分类文件。
#import "UIImageView+WebCache.h"
#import "objc/runtime.h"
// 3个动态添加的属性
static char imageURLKey; // 图片url
static char operationKey; // 操作
static char operationArrayKey; // 一组操作
@implementation UIImageView (WebCache)
- (void)setImageWithURL:(NSURL *)url {
[self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
[self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options {
[self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock {
[self setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}
这段代码值得说的是,所有暴露在外的接口方法内部其实都是在调用一个参数最多的一个方法,我们暂且将其称为
全能方法
。这样的话将核心代码封装在一处,不必写冗余饿的代码,且看起来一目了然。
我们自己在写一个自定义控件或者说自己写东西时,提供的初始化方法和功能方法最好都以此种形式写。
另外,该分类声明了三个静态字符,均用于通过运行时动态地给分类添加属性,也叫属性关联。分类中是不可以直接定义属性的,需要通过运行时添加。
接下来我们跳入全能方法
观察它做了什么。首先,我们看到该方法有五个参数,分别是图片url
,占位图placeholder
,配置选项options
,下载进行中的进度block回调progressBlock
,下载完成后的completedBlock
回调。
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedBlock)completedBlock;
然后,我们开始一步一步的观察此方法内部做了什么:
- (void)setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedBlock)completedBlock {
[self cancelCurrentImageLoad]; // 取消当前的operation(取消操作并设置operationKey属性为nil)
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 设置imageURLKey属性
self.image = placeholder; // 先设置占位图
if (!(options & SDWebImageDelayPlaceholder)) { // SDWebImageDelayPlaceholder枚举值的含义是取消网络图片加载好前展示占位图片。所以在这里并不能直接把placeholder直接赋值给self.image,而要用if条件排除这种情况。
self.image = placeholder;
}
if (url) {
// 获取图片(先从内存找,若无再从磁盘找,若仍无则从网络下载。)
// 先从内存找,若找到,则在主线程设置图片,并调用block回调;若在磁盘找到,则还要将其添加到内存缓存再做后续的;若是从网络下载而得,则内存和磁盘均要添加缓存,而后再进行后续的事。
__weak UIImageView *wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
if (!wself) return;
dispatch_main_sync_safe(^{ // dispatch_main_sync_safe宏的目的是确保一定在主线程
if (!wself) return;
if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) { // 这个条件在这里又出现了,SDWebImageDelayPlaceholder的含义是网络图片加载前不显示占位图片,但此时网络图片没有成功请求到,所以此时需要加载占位图。
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType);
}
});
}];
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 获取图片的同时,将该操作赋为UIImageView+WebCache的属性。
}
}
该方法内部一上来首先就调用了方法
[self cancelCurrentImageLoad];
取消了当前UIImageView
对象的获取图片操作;然后将参数url设为该分类的一个属性;接着设置了UIImageView
对象的占位图;然后便调用了SDWebImageManager
的实例对象方法获取图片,在其回调block里,若获取图片成功,则将其设为UIImageView
的图片,若获取失败则将placeholder
赋为占位图,并且最后将完成的block回调。(尽管我们通常使用的是没有回调的接口,若改为有回调的接口,下载完成后是可以收到回调的)
SDWebImageManager
的获取图片的对象方法是异步下载图片的,该方法return
的是id <SDWebImageOperation>
类型的对象,它代表一个实现了SDWebImageOperation
协议的任何对象,在这里表示获取图片这一操作
。在调用了该方法获取图片的后,旋即我们便将该方法的返回值,即该operation
赋为该分类的属性。至于为什么要将其赋为属性?因为它代表获取图片的“操作”,我们要允许后续将获取图片的操作取消。(下面马上会讲解“取消当前获取图片的操作”的方法)
objc_setAssociatedObject(self, &operationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
上面的代码有几处仍需要细说,或者说留意一下。
First:一开始调用的取消当前获取图片的操作那个方法内部是这样实现的:
// 取消当前UIImageView对象的获取图片操作,并将分类的该属性赋为nil
- (void)cancelCurrentImageLoad {
// Cancel in progress downloader from queue
id <SDWebImageOperation> operation = objc_getAssociatedObject(self, &operationKey);
if (operation) {
[operation cancel];
objc_setAssociatedObject(self, &operationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
即通过运行时get到当前操作的属性对象operation
,然后它调用了[operation cancel];
方法,其实这个cancel
方法是这么回事:获取图片的方法返回的是“获取图片的操作”——operation
对象,这个operation
是实现了SDWebImageOperation
协议的一个对象。其实在获取图片方法内部我们就可以看到(后面将会说到)该对象是类SDWebImageCombinedOperation
创建的,它正是实现了SDWebImageOperation
协议,并实现了cancel
方法,在其中进行了更多的取消处理(不仅是调用了NSOperation
的cancel
方法)。
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
self.cancelBlock = nil;
}
}
** Second:**SDWebImageDelayPlaceholder
这个枚举值的意思是取消网络图片加载好之前展示占位图片,即网络图片加载好之前不显示占位图。
if (!(options & SDWebImageDelayPlaceholder)) {
self.image = placeholder;
}
在这里并不能直接把placeholder直接赋值给self.image,而要用if条件排除这种情况。然而在网络下载图片的回调里,若没有得到图片的情况下又有段这样的代码:
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
此时网络图片没有成功请求到,而占位图此前又没有设置,所以此时单就这种情形需要加载占位图。
** Third:**在获取图片的block回调里我们可以看到,代码中使用的是dispatch_main_sync_safe
,它是被定义的一个宏,目的是为了保证是在主线程的。因为设置图片这些UI操作,必须得在主线程进行。
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
}\
else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}
SDWebImageManager
然后我们点击downloadWithURL:options: progress: completed:
方法便跳入了SDWebImageManager
类中。该类是封装了获取图片的整个动作。我们先来看看SDWebImageManager.h
头文件的代码吧。
@interface SDWebImageManager : NSObject
@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache; // 关于缓存
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader; // 关于下载
@property (strong) NSString *(^cacheKeyFilter)(NSURL *url);
+ (SDWebImageManager *)sharedManager;
- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;
/**
* Saves image to cache for given URL
*
* @param image The image to cache
* @param url The URL to the image
*
*/
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
/**
* Cancel all current opreations
*/
- (void)cancelAll;
/**
* Check one or more operations running
*/
- (BOOL)isRunning;
/**
* Check if image has already been cached
*/
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (BOOL)diskImageExistsForURL:(NSURL *)url;
/**
*Return the cache key for a given URL
*/
- (NSString *)cacheKeyForURL:(NSURL *)url;
@end
可以看到属性中有两个是非常重要的:imageCache
和imageDownloader
分别代表对缓存的处理和对下载的处理。同样下面也定义好几个方法,都自带了注释。
接下来我们重点看downloadWithURL:options: progress: completed:
方法的实现。代码太长就不全部列出了,只列出主要代码:
- (id <SDWebImageOperation>)downloadWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletedWithFinishedBlock)completedBlock;
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;
}
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 该方法return的就是该operation实例对象
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url]; // 判断该url是否已在“黑名单”内
}
// 对于url为空或在黑名单内且不允许再次请求配置的情况,则直接在此时用block回调,并抛出错误,而不用费力做后续步骤。
if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES);
});
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation]; // 将当前操作,添加进runningOperations数组,表示当前正在进行的操作们
}
NSString *key = [self cacheKeyForURL:url]; // 转换为url的字符串,图片在缓存中是以url字符串为key存储的。
// 先从缓存中查找。返回NSOperation类型的operation对象
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache bug SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES); // 有图片就通过block回调。
});
}
...
...
首先一开始对参数的校验及处理,我们自己写代码也一定要养成这样严谨的代码习惯。在方法内部首先对参数进行非空或者其他一些校验处理,一来可以防止因为参数值为nil引起的崩溃,二来若参数为nil,则大多数情况下可以直接
return
了,不必再费力往下执行了。同样的道理,下面判断了该url
是否在黑名单内,若在黑名单内,并且是“不允许再次请求”配置的情况下,就直接在此用block回调,并抛出错误,而不用费力执行后续动作。
然后我们看到通过
SDWebImageCombinedOperation
类创建了operation
对象,该方法return
的就是该对象,SDWebImageCombinedOperation
类就是一个实现了SDWebImageOperation
协议的类。
旋即将该
operation
对象加入了self.runningOperations
数组,表示当前正在进行的操作们。
然后我们以url的字符串为key,调用
SDImageCache
的实例方法queryDiskCacheForKey:key done:
从缓存获取获取图片,若获取成功则将其回调。
若从缓存中没有找到图片,则继续往下执行至此,则开始要从网络请求图片了。
// 执行到此,说明缓存中并未找到图片,所以得从网络下载。
// 该方法进行异步下载图片,并返回了一个subOperation对象,它是一个实现了SDWebImageOperation协议的对象
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
...
...
...
}];
然后在completed
回调里,若下载成功,则首先将该图片存入缓存,然后回调至主线程。
// 下载成功后,将图片添加到缓存中,并通过block回调。
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished);
});
并且,若该下载操作已完成,则runningOperations数组中移除该operation:
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
并且实现了operation
的cancelBlock
block回调:
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
结尾
SDWebImage
的整体脉络结构,及它加载图片的思路就是以上了,看那张图更简洁清楚。但是这些也只是大体的脉络而已,还有最重要的缓存部分,以及网络请求部分都还没写,留在后续,分别另开两篇研究其缓存和网络部分。