【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
四、图片下载层
SDWebImageDownloader
是处理图片下载的类
4.1 图片下载步骤
首先介绍下dispatch_barrier
,GCD
中的dispatch_barrier
目的是在并发队列
实现串行
的效果,创建下载任务SDWebImageDownloaderOperation
是以同步任务(dispatch_barrier_sync
)+并发队列(barrierQueue
)的方式创建的,因为需要实时返回,所以需要使用同步任务的方式。这里为什么是使用dispatch_barrier_sync
,因为cancel
方法取消操作是以异步任务(dispatch_barrier_async
)+并发队列(barrierQueue
)的方式取消下载任务,dispatch_barrier_sync
和dispatch_barrier_async
是配对使用的,目的是为了确保取消下载Operation的步骤是在创建下载Operation之后的。
对GCD
技术不了解的同学可以参考 GCD(一) 队列、任务、串行、并发 、 GCD(二) dispatch_barrier 这两篇文章的介绍
SDWebImageDownloader
中的处理流程如下
- 以同步任务(
dispatch_barrier_sync
)+并发队列(barrierQueue
)的方式创建下载任务SDWebImageDownloaderOperation
- 添加下载任务
SDWebImageDownloaderOperation
到URLOperations
数组中,注册SDWebImageDownloaderOperation
的完成回调,在回调中吧任务从URLOperations
中删除。URLOperations
数组的任务是确保同一个URL同时只有一个下载任务SDWebImageDownloaderOperation
进行下载 - 保存到回调Block(进度回调和结束回调)到下载任务
SDWebImageDownloaderOperation
的回调数组中 - 返回token,token中包含了下载URL和回调Block,
SDWebImage
的SDWebImageManager
图片管理单例对象取消下载会使用到这个token,目前只有使用到token中的url
真实发起请求以及处理数据地方是在下载任务SDWebImageDownloaderOperation
中,使用NSURLSession
发起请求,使用NSURLSessionTask
处理数据。
SDWebImageDownloaderOperation
下载的具体流程如下图所示:

4.2 取消处理
图片管理层有个SDWebImageCombinedOperation
对象,它持有图片缓存的NSOperation
以及他本身cancel
操作的回调cancelBlock
,cancelBlock
中持有subOperationToken
,也就是下载管理器返回的token
,使用这个token
,调用下载管理器SDWebImageDownloader
的cancel
方法取消下载的Operation
取消下载通信图如下所示:

这里涉及到许多Block,所以需要特别注意有可能导致的循环引用问题,比如某个对象的block调用中使用了自身,需要使用__weak防止循环引用,下面是SDWebImageManager
中的loadImageWithURL:options:progress:completed:
方法处理循环引用的方式
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
4.3 后台下载任务
SDWebImage
中提供了一个选项用于支持后台下载SDWebImageDownloaderContinueInBackground
,在自带的Demo中可以修改代码如下,来查看代码的调用
cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row];
SDWebImageOptions options = indexPath.row == 0 ? SDWebImageRefreshCached : 0;
// 添加后台下载选项
options |= SDWebImageContinueInBackground;
[cell.customImageView sd_setImageWithURL:[NSURL URLWithString:_objects[indexPath.row]]
placeholderImage:placeholderImage
options:options];
开启后台任务,是在调用[dataTask resume]
之前,官方文档要求是越早越好,否则可能会出现异常
Call this method as early as possible before starting your task, and preferably before your app actually enters the background.
下面是开启后台下载任务的关键代码:
// 执行后台任务,在下载任务开始的时候马上调用
// beginBackgroundTaskWithExpirationHandler回调中调用`endBackgroundTask`结束后台任务,并且设置`backgroundTaskId`值为`UIBackgroundTaskInvalid`
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
// 正常下载流程 ......
// 如果已经在前台执行了下载任务,那么调用`endBackgroundTask`结束后台任务,并且设置`backgroundTaskId`值为`UIBackgroundTaskInvalid`
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
4.4 下载选项
- 优先级
SDWebImageDownloaderLowPriority
低优先级SDWebImageDownloaderHighPriority
高优先级
这两个选项是用于设置SDWebImageDownloaderOperation
的优先级的
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
通过SDWebImagemanager
调用对应的是SDWebImageLowPriority
和SDWebImageHighPriority
这两个值。
- 渐进式加载
SDWebImageDownloaderProgressiveDownload
如果设置了该选项,在- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
回调方法中就会处理图像数据,通过progressBlock返回图像,会有性能损耗,默认是关闭的。通过SDWebImagemanager
调用对应的是SDWebImageProgressiveDownload
值。
SDWebImageDownloaderUseNSURLCache
使用URL缓存选项
如果设置看该选项,创建NSMutableURLRequest
的时候cachePllicy
会使用NSURLRequestUseProtocolCachePolicy
,也就是会使用URL的缓存策略,否则使用NSURLRequestReloadIgnoringLocalCacheData
忽略缓存。通过SDWebImagemanager
调用对应的是SDWebImageRefreshCached
这个值。
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
SDWebImageDownloaderContinueInBackground
后台下载选项
参考之前提到的后台下载任务
,默认是不开启的 ,通过SDWebImagemanager
调用对应的是SDWebImageLowPriority
和SDWebImageContinueInBackground
这两个值。
SDWebImageDownloaderScaleDownLargeImages
压缩大图选项
这个选项会对解码之后操作60M的图像进行压缩,压缩处理到60M,每次从源图像中取出大小为20M大小的部分,调用CGContextDrawImage
绘制到目标图像中,循环直到没有数据位置,下面是图像压缩数据处理的数据图,一张大小为90M的图像,每次取20M大小的图像,写入到目标图像文件中,每次写入到目标图像的大小大概为13.2M,最后一次取出的源文件大小为10M,写入到目标文件大小为0.66M,写入结束之后调用CGBitmapContextCreateImage
取出目标图像,完成压缩。

具体的实现代码如下,原来的代码比较长,删除大部分剩余关键代码如下
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
CGContextRef destContext;
// 使用autoreleasepool防止高内存峰值
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGImageRef sourceTileImageRef;
// 计算循环获取的次数
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
// 获取原图像中的部分,计算对应目标图像中的Frame
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
// 最后一次的高度做特殊处理
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
// 绘制部分图像
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
// 获取最终图像
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
return destImage;
}
}
4.5 预下载
SDWebImagePrefetcher
提供了预下载的功能,最终也是通过SDWebImageManager
处理下载,这样统一了底层的下载逻辑,如果同时存在预下载操作和正常下载操作,底层只会有一个下载操作,可以有多个回调回调数据。预下载是串行处理的,可以通过以下两个方法进行预下载。
-
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls
-
- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock
来源:oschina
链接:https://my.oschina.net/FEEDFACF/blog/3143039