一、概念
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。
二、优势
自从iOS5.0开始,NSURLCache也可以处理磁盘缓存,那么SDWebImage的优势在哪?
- 首先NSURLCache是缓存原始数据(raw data)到磁盘或内存,因此每次使用的时候需要将原始数据转换成具体的对象,如UIImage等,这会导致额外的数据解析以及内存占用等,而SDWebImage则是缓存UIImage对象在内存,缓存在NSCache中,同时直接保存压缩过的图片到磁盘中;
- 第一次在UIImageView中使用image对象的时候,图片的解码是在主线程中运行的!而SDWebImage会强制将解码操作放到子线程中。
三、功能
- 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
- 一个异步的图片加载器
- 一个异步的内存+磁盘图片缓存
- 支持GIF图片
- 支持WebP图片
- 后台图片解压缩处理
- 确保同一个URL的图片不被下载多次
- 确保虚假的URL不会被反复加载
- 确保下载及缓存时,主线程不被阻塞
- 保证主线程不会死锁
- 使用GCD和ARC
四、源码解析
1、下载
在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的。它是一个异步下载器,并对图像加载做了优化处理。下面我们就来看看它的具体实现。
1.1 下载选项
在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举SDWebImageDownloaderOptions定义,具体如下
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 默认情况下请求不使用NSURLCache,如果设置该选项,则以默认的缓存策略来使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果从NSURLCache缓存中读取图片,则使用nil作为参数来调用完成block
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 在iOS 4+系统上,允许程序进入后台后继续下载图片。该操作通过向系统申请额外的时间来完成后台下载。如果后台任务终止,则操作会被取消
SDWebImageDownloaderContinueInBackground = 1 << 4,
// 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES来处理存储在NSHTTPCookieStore中的cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允许不受信任的SSL证书。主要用于测试目的。
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 将图片下载放到高优先级队列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
可以看出,这些选项主要涉及到下载的优先级、缓存、后台任务执行、cookie处理以及认证几个方面。
1.2 下载顺序
SDWebImage的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序
SDWebImageDownloaderFIFOExecutionOrder,
// 以栈的方式,按照后进先出的顺序下载。
SDWebImageDownloaderLIFOExecutionOrder
};
1.3 下载管理器
SDWebImageDownloader下载管理器是一个单例类,它主要负责图片的下载操作的管理。图片的下载是放在一个NSOperationQueue操作队列中来完成的,其声明如下:
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
默认情况下,队列最大并发数是6。如果需要的话,我们可以通过SDWebImageDownloader类的 maxConcurrentDownloads 属性来修改。
所有下载操作的网络响应序列化处理是放在一个自定义的并行调度队列中来处理的,其声明及定义如下:
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
- (id)init {
if ((self = [super init])) {
...
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
...
}
return self;
}
每一个图片的下载都会对应一些回调操作,如下载进度回调,下载完成回调等,这些回调操作是以block形式来呈现,为此在SDWebImageDownloader.h中定义了几个block,如下所示:// 下载进度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下载完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
// Header过滤
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
图片下载的这些回调信息存储在SDWebImageDownloader类的 URLCallbacks 属性中,该属性是一个字典,key是图片的URL地址,value则是一个数组,包含每个图片的多组回调信息。由于我们允许多个图片同时下载,因此可能会有多个线程同时操作URLCallbacks属性。为了保证URLCallbacks操作(添加、删除)的线程安全性,SDWebImageDownloader将这些操作作为一个个任务放到barrierQueue队列中,并设置屏障来确保同一时间只有一个线程操作URLCallbacks属性,我们以添加操作为例,如下代码所示:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
...
// 1. 以dispatch_barrier_sync操作来保证同一时间只有一个线程能对URLCallbacks进行操作
dispatch_barrier_sync(self.barrierQueue, ^{
...
// 2. 处理同一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;
...
});
}
整个下载管理器对于下载请求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面来处理的,该方法调用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法来将请求的信息存入管理器中,同时在创建回调的block中创建新的操作,配置之后将其放入downloadQueue操作队列中,最后方法返回新创建的操作。其具体实现如下:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
...
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
...
// 1. 创建请求对象,并根据options参数设置其属性
// 为了避免潜在的重复缓存(NSURLCache + SDImageCache),如果没有明确告知需要缓存,则禁用图片请求的缓存操作
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
...
// 2. 创建SDWebImageDownloaderOperation操作对象,并进行配置
// 配置信息包括是否需要认证、优先级
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 3. 从管理器的callbacksForURL中找出该URL所有的进度处理回调并调用
...
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 4. 从管理器的callbacksForURL中找出该URL所有的完成处理回调并调用,
// 如果finished为YES,则将该url对应的回调信息从URLCallbacks中删除
...
if (finished) {
[sself removeCallbacksForURL:url];
}
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 5. 取消操作将该url对应的回调信息从URLCallbacks中删除
SDWebImageDownloader *sself = wself;
if (!sself) return;
[sself removeCallbacksForURL:url];
}];
...
// 6. 将操作加入到操作队列downloadQueue中
// 如果是LIFO顺序,则将新的操作作为原队列中最后一个操作的依赖,然后将新操作设置为最后一个操作
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
另外,每个下载操作的超时时间可以通过downloadTimeout属性来设置,默认值为15秒。
1.4 下载操作
每个图片的下载都是一个Operation操作。我们在上面分析过这个操作的创建及加入操作队列的过程。现在我们来看看单个操作的具体实现。
SDWebImage定义了一个协议,即 SDWebImageOperation 作为图片下载操作的基础协议。它只声明了一个cancel方法,用于取消操作。协议的具体声明如下:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImage自定义了一个Operation类,即 SDWebImageDownloaderOperation,它继承自NSOperation,并采用了SDWebImageOperation协议。除了继承而来的方法,该类只向外暴露了一个方法,即上面所用到的初始化方法initWithRequest:options:progress:completed:cancelled:。
对于图片的下载,SDWebImageDownloaderOperation完全依赖于URL加载系统中的NSURLConnection类(并未使用7.0以后的NSURLSession类)。我们先来分析一下SDWebImageDownloaderOperation类中对于图片实际数据的下载处理,即NSURLConnection各代理方法的实现。
首先,SDWebImageDownloaderOperation在分类中采用了NSURLConnectionDataDelegate协议,并实现了该协议的以下几个方法:
- connection:didReceiveResponse:
- connection:didReceiveData:
- connectionDidFinishLoading:
- connection:didFailWithError:
- connection:willCacheResponse:
- connectionShouldUseCredentialStorage:
- connection:willSendRequestForAuthenticationChallenge:
我们在此不逐一分析每个方法的实现,就重点分析一下-connection:didReceiveData:方法。该方法的主要任务是接收数据。每次接收到数据时,都会用现有的数据创建一个CGImageSourceRef对象以做处理。在首次获取到数据时(width+height==0)会从这些包含图像信息的数据中取出图像的长、宽、方向等信息以备使用。而后在图片下载完成之前,会使用CGImageSourceRef对象创建一个图片对象,经过缩放、解压缩操作后生成一个UIImage对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 1. 附加数据
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// 2. 获取已下载数据总大小
const NSInteger totalSize = self.imageData.length;
// 3. 更新数据源,我们需要传入所有数据,而不仅仅是新数据
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
// 4. 首次获取到数据时,从这些数据中获取图片的长、宽、方向属性值
if (width + height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
...
CFRelease(properties);
// 5. 当绘制到Core Graphics时,我们会丢失方向信息,这意味着有时候由initWithCGIImage创建的图片
// 的方向会不对,所以在这边我们先保存这个信息并在后面使用。
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
// 6. 图片还未下载完成
if (width + height > 0 && totalSize < self.expectedSize) {
// 7. 使用现有的数据创建图片对象,如果数据中存有多张图片,则取第一张
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 8. 适用于iOS变形图像的解决方案。我的理解是由于iOS只支持RGB颜色空间,所以在此对下载下来的图片做个颜色空间转换处理。
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
// 9. 对图片进行缩放、解码操作
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
image = [UIImage decodedImageWithImage:scaledImage];
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
注:缩放操作可以查看SDWebImageCompat文件中的SDScaledImageForKey函数;
解压缩操作可以查看SDWebImageDecoder文件+decodedImageWithImage方法
//SDWebImageCompat
//兼容类,这个类定义了很多宏还有一个伸缩图片的方法,宏就不说了
//这个方法定义成C语言式的内联方法核心代码如下,传入key和图片,如果key中出现@2x就设定scale为2.0,出现@3x就设定scale为3.0,然后伸缩图片
CGFloat scale = [UIScreen mainScreen].scale;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
//SDWebImageDecoder
//这个是解码器类,只定义了一个解码方法,传入图片,返回的也是图片
//CGImageRef是一个指针类型。
//typedef struct CGImage *CGImageRef;获取传入图片的alpha信息,然后判断是否符合苹果定义的CGImageAlphaInfo,如果是就返回原图片
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
if (anyAlpha) { return image; }
//然后获取图片的宽高和color space(指定颜色值如何解释),判断color space是否支持,不支持就转换为支持的模式(RGB),再用图形上下文根据获得的信息画出来,释放掉创建的CG指针再返回图片
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace)
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, width,
height,
CGImageGetBitsPerComponent(imageRef),
0,
colorspaceRef,
kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation];
if (unsupportedColorSpace)
CGColorSpaceRelease(colorspaceRef);
CGContextRelease(context);
CGImageRelease(imageRefWithAlpha);
return imageWithAlpha;
我们前面说过SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态。
在start方法中,创建了我们下载所使用的NSURLConnection对象,开启了图片的下载,同时抛出一个下载开始的通知。当然,如果我们期望下载在后台处理,则只需要配置我们的下载选项,使其包含SDWebImageDownloaderContinueInBackground选项。start方法的具体实现如下:
- (void)start {
@synchronized (self) {
// 管理下载状态,如果已取消,则重置当前下载并设置完成状态为YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 1. 如果设置了在后台执行,则进行后台执行
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
...
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
// 2. 在主线程抛出下载开始通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
// 3. 启动run loop
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
// 4. 如果未完成,则取消连接
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
...
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
当然,在下载完成或下载失败后,需要停止当前线程的run loop,清除连接,并抛出下载停止的通知。如果下载成功,则会处理完整的图片数据,对其进行适当的缩放与解压缩操作,以提供给完成回调使用。具体可参考-connectionDidFinishLoading:与-connection:didFailWithError:的实现。
1.5 小结
下载的核心其实就是利用NSURLConnection对象来加载数据。每个图片的下载都由一个Operation操作来完成,并将这些操作放到一个操作队列中。这样可以实现图片的并发下载。
参考:
- http://southpeak.github.io/blog/2015/02/07/yuan-ma-pian-:sdwebimage/?utm_source=tuicool&utm_medium=referral
- http://blog.sina.com.cn/s/blog_8988732e0101af25.html
- http://www.jianshu.com/p/c07df06c60be
- http://www.zuimoban.com/jiaocheng/ios/2016/0310/6794.html