SDWebImage下载模块解析
在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山。它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件。在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。关于 SDWebImage的源码解读可以参考我的 另一篇博客iOS图片加载框架-SDWebImage解读.本篇文章的重点主要放在SDWebImage框架的图片下载模块解读。
Paste_Image.png从 SDWebImage的Architecture图中可以看到,SDWebImage框架的图片下载模块主要是以SDWebImageDownloader为主。那么我们就可以沿着SDWebImageDownloader这个切入点逐步去探索整个图片下载流程和细节。
SDWebImageDownloader 是一个下载管理器
SDWebImageDownloader类图从SDWebImageDownloader的类图信息可以清晰的看出来,SDWebImageDownloader的主要任务是下载方面管理,包括下载队列的先后顺序,最大下载任务数量控制,下载队列中的任务创建,取消,暂停等任务管理,以及其他 HTTPS 和 HTTP Header的设置。
//传入图片的URL,图片下载过程的Block回调,图片完成的Block回调
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// 传入对应的参数,addProgressCallback: completedBlock: forURL: createCallback: 方法
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
// Header的设置
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
//创建 SDWebImageDownloaderOperation,这个是下载任务的执行者。并设置对应的参数
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
//用于请求认证
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
//下载任务优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
//加入任务的执行队列
[sself.downloadQueue addOperation:operation];
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
该方法主要操作在于创建一个SDWebImageDownloaderOperation 对象,然后对该进行属性设置,再将该对象加入名字为downloadQueue 的队列。
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
dispatch_barrier_async(self.barrierQueue, ^{
//根据token取出对应的SDWebImageDownloaderOperation对象然后调用该对象的cancel: 方法
SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:token.url];
}
});
}
有创建SDWebImageDownloaderOperation 对象,自然也有取消方法。该方法从保存SDWebImageDownloaderOperation对象的字典中取出token对应的SDWebImageDownloaderOperation对象,然后调用该对象的取消方法。
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
if ((self = [super init])) {
// ...
sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
在SDWebImageDownloader的初始化方法中有创建了一个NSURLSession,这个对象是用来执行图片下载的网络请求的,它的代理对象是SDWebImageDownloader。所以我们在SDWebImageDownloader类里面找到对应的NSURLSession的代理方法,那么就可以看到下载操作的一些细节。
#pragma mark NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
// 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
// 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
// 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
[dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
}
#pragma mark NSURLSessionTaskDelegate
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
// 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
[dataOperation URLSession:session task:task didCompleteWithError:error];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
completionHandler(request);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
// 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
[dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
}
从上面的NSURLSession的各个回调方法中的操作可以看到,所有的方法都是被转发到SDWebImageDownloaderOperation对象,该对象隐藏在SDWebImageDownloader背后默默的干活,这个也符合我们封装的原则。
SDWebImageDownloaderOperation 是下载任务处理者
SDWebImageDownloaderOperation.png从这个架构图中可以看出SDWebImageDownloaderOperation 继承于 NSOperation ,实现了SDWebImageOperation和SDWebImageDownloaderOperationInterface协议。当然最重要的还是要看NSURLSession的回调方法处理。
//收到响应
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
//'304 Not Modified' is an exceptional one
// (没有statusCode) 或者 (statusCode小于400 并且 statusCode 不等于304)
// 若是请求响应成功,statusCode是200,那么会进入这个代码分支
if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
//期望收到的数据量
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
//下载过程回调,收到数据量为0
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
//根据期望收到的数据量创建NSMutableData,该NSMutableData用户保存收到的数据量
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
//保存响应对象
self.response = response;
//发送网络请求收到响应的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
else {
NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
//This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
//In case of 304 we need just cancel the operation and return cached image from the cache.
if (code == 304) {
//code 304 取消下载 直接返回缓存中的内容
[self cancelInternal];
} else {
//数据请求任务取消
[self.dataTask cancel];
}
//发送停止下载的通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
// 错误处理回调
[self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
[self done];
}
// 调用completionHandler回调
if (completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
收到网络请求的响应之后判断statusCode,若是正常的statusCode,那么就根据数据量创建对应大小的NSMutableData,发送对应的通知。若是出现异常,那么就做好对应的错误回调和发送对应的错误通知。
// 收到请求数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//组装请求数据
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
//渐进式加载图片处理
}
//下载过程回调
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
}
}
不断收到响应数据,不断地调用下载过程的回调Block.
//数据请求任务完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
@synchronized(self) {
self.dataTask = nil;
dispatch_async(dispatch_get_main_queue(), ^{
// 发送停止下载通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
if (!error) {
//发送完成下载通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
}
});
}
if (error) {
//错误回调
[self callCompletionBlocksWithError:error];
} else {
if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
/**
* See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
* Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
* and images for which responseFromCached is YES (only the ones that cannot be cached).
* Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
*/
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
// hack
[self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
} else if (self.imageData) {
//处理图片方向和图片格式
UIImage *image = [UIImage sd_imageWithData:self.imageData];
// 获取缓存key
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 处理图片的缩放倍数
image = [self scaledImageForKey:key image:image];
// Do not force decoding animated GIFs
// 动图不要解压
if (!image.images) {
if (self.shouldDecompressImages) {
if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
image = [UIImage decodedAndScaledDownImageWithImage:image];
[self.imageData setData:UIImagePNGRepresentation(image)];
#endif
} else {
image = [UIImage decodedImageWithImage:image];
}
}
}
// 回调处理
if (CGSizeEqualToSize(image.size, CGSizeZero)) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
}
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
}
}
}
// 完成
[self done];
}
图片数据下载完成之后,最重要的一件事就是将图片数据转成UIImage对象。然后通过 - (void)callCompletionBlocksWithImage:imageData:error:finished: 方法 将该UIImage对象和图片数据的data 传给SDWebImageDownloader。
图片数据下载完成后,还需要生成UIImage对象
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
if (!data) {
return nil;
}
UIImage *image;
//识别图片格式
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatGIF) {
// gif图片处理,得到静态图片
image = [UIImage sd_animatedGIFWithData:data];
}
//是否开启了 WEBP 格式支持
#ifdef SD_WEBP
// webp图片处理
else if (imageFormat == SDImageFormatWebP)
{
// 得到webp格式图片
image = [UIImage sd_imageWithWebPData:data];
}
#endif
// 静态图片处理
else {
image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
//图片方向处理
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
// 图片的方向默认是UIImageOrientationUp,若是本来图片的方向就是UIImageOrientationUp那么就不需要重新处理图片的方向
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
#endif
}
return image;
}
识别出对应的图片格式,对GIF,WEBP格式做正确的处理。识别出图片的方向,然后生成方向正确的图片。值得注意的是在SDWebImage 4.0 之后 GIF 图片使用 FLAnimatedImageView 来支持。
// 返回正确的缩放倍数图片
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
if (!image) {
return nil;
}
#if SD_MAC
return image;
#elif SD_UIKIT || SD_WATCH
// 动图处理
if ((image.images).count > 0) {
NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];
for (UIImage *tempImage in image.images) {
//递归处理
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
// 处理动图播放
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
}
else {
#if SD_WATCH
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#endif
//判断图片的缩放倍数,默认为 1
CGFloat scale = 1;
// 判断key的命名规则是否有包含缩放倍数
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对象
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
#endif
}
除了图片格式和图片方向。SDWebImage还可以对@2x,@3x后缀的图片做正确的缩放处理。
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
//图片是否可以解压,若是不适合解压则直接返回原图片
if (![UIImage shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
//CGColorSpaceRef : A profile that specifies how to interpret a color value for display.
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
//宽度
size_t width = CGImageGetWidth(imageRef);
//高度
size_t height = CGImageGetHeight(imageRef);
//kBytesPerPixel 每个像素有4个字节
//图片每行的数据量
size_t bytesPerRow = kBytesPerPixel * width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
// kBitsPerComponent(一个颜色由几个bit表示) : the number of bits allocated for a single color component of a bitmap image.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
// 在这个图片处理过程中丢失了图片的透明度信息
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
// 没有透明度信息的图片
return imageWithoutAlpha;
}
}
在上述方法的处理过程中,会丢失图片的Alpha信息,但是SDWebImage在处理之前有保存了这个Alpha信息,处理OK,之后生成的UIImage对象是包含正确的Alpha信息。SDWebImage把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。这个是可以避免系统的UIImage缓存,这也是常见的网络图片库的做法。
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
//是否需要解码图片
if (![UIImage shouldDecodeImage:image]) {
return image;
}
//是否需要缩小图片
if (![UIImage shouldScaleDownImage:image]) {
//不需要缩小的图片则进行图片解码
return [UIImage decodedImageWithImage:image];
}
//SDWebImage中的图片缩小的代码来自Apple的官方Demo,里面有比较详细的注释 https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html
}
可以通过设置下载选项,让SDWebImage对超过特定大小的图片进行缩小,然后再存入缓存。该图片缩小的代码来自Apple 的官方 Demo 不再做详细注解。
部分架构图SDWebImageDownloader拿到下载后的图片data和UIImage对象之后,将这些内容返回给SDWebImageManager,SDWebImageManager将UIImage对象存入SDImageCache缓存中。到这里SDWebImage的下载模块的任务就全部完成了。
个人能力有限,若是大家有发现文章中的不足或者错误之处,恳请在评论中指出,我会进行修正。
参考资料
SDWebImage 项目 https://github.com/rs/SDWebImage
iOS 处理图片的一些小 Tip http://blog.ibireme.com/2015/11/02/ios_image_tips/
Apple 官方 Demo(LargeImageDownsizing) https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html
}