标签:
源码来源: https://github.com/rs/SDWebImage
版本: 3.7
SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:
从github上对SDWebImage使用情况就可以看出,SDWebImage在图片下载及缓存的处理方面还是很被认可的。在本文中,我们主要从源码的角度来分析一下SDWebImage的实现机制。讨论的内容将主要集中在图片的下载及缓存,而不包含对GIF图片及WebP图片的支持操作。
在SDWebImage中,图片的下载是由SDWebImageDownloader类来完成的。它是一个异步下载器,并对图像加载做了优化处理。下面我们就来看看它的具体实现。
在下载的过程中,程序会根据设置的不同的下载选项,而执行不同的操作。下载选项由枚举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处理以认证几个方面。
SDWebImage的下载操作是按一定顺序来处理的,它定义了两种下载顺序,如下所示
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 以队列的方式,按照先进先出的顺序下载。这是默认的下载顺序
SDWebImageDownloaderFIFOExecutionOrder,
// 以栈的方式,按照后进先出的顺序下载。
SDWebImageDownloaderLIFOExecutionOrder
};
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秒。
每个图片的下载都是一个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对象供完成回调使用。当然,在这个方法中还需要处理的就是进度信息。如果我们有设置进度回调的话,就调用这个进度回调以处理当前图片的下载进度。
注:缩放操作可以查看SDWebImageCompat文件中的SDScaledImageForKey函数;解压缩操作可以查看SDWebImageDecoder文件+decodedImageWithImage方法
- (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);
}
});
}
}