大话SDWebImage(三)-- 图片下载层

删除回忆录丶 提交于 2019-12-14 17:32:24

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

四、图片下载层

SDWebImageDownloader是处理图片下载的类

4.1 图片下载步骤

首先介绍下dispatch_barrierGCD中的dispatch_barrier目的是在并发队列实现串行的效果,创建下载任务SDWebImageDownloaderOperation是以同步任务(dispatch_barrier_sync)+并发队列(barrierQueue)的方式创建的,因为需要实时返回,所以需要使用同步任务的方式。这里为什么是使用dispatch_barrier_sync,因为cancel方法取消操作是以异步任务(dispatch_barrier_async)+并发队列(barrierQueue)的方式取消下载任务,dispatch_barrier_syncdispatch_barrier_async是配对使用的,目的是为了确保取消下载Operation的步骤是在创建下载Operation之后的。
GCD技术不了解的同学可以参考 GCD(一) 队列、任务、串行、并发GCD(二) dispatch_barrier 这两篇文章的介绍

SDWebImageDownloader中的处理流程如下

  • 以同步任务(dispatch_barrier_sync)+并发队列(barrierQueue)的方式创建下载任务SDWebImageDownloaderOperation
  • 添加下载任务SDWebImageDownloaderOperationURLOperations数组中,注册SDWebImageDownloaderOperation的完成回调,在回调中吧任务从URLOperations中删除。URLOperations数组的任务是确保同一个URL同时只有一个下载任务SDWebImageDownloaderOperation进行下载
  • 保存到回调Block(进度回调和结束回调)到下载任务SDWebImageDownloaderOperation的回调数组中
  • 返回token,token中包含了下载URL和回调Block,SDWebImageSDWebImageManager图片管理单例对象取消下载会使用到这个token,目前只有使用到token中的url

真实发起请求以及处理数据地方是在下载任务SDWebImageDownloaderOperation中,使用NSURLSession发起请求,使用NSURLSessionTask处理数据。

SDWebImageDownloaderOperation下载的具体流程如下图所示:

4.2 取消处理

图片管理层有个SDWebImageCombinedOperation对象,它持有图片缓存的NSOperation以及他本身cancel操作的回调cancelBlockcancelBlock中持有subOperationToken,也就是下载管理器返回的token,使用这个token,调用下载管理器SDWebImageDownloadercancel方法取消下载的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 下载选项

  1. 优先级
    • SDWebImageDownloaderLowPriority 低优先级
    • SDWebImageDownloaderHighPriority 高优先级

这两个选项是用于设置SDWebImageDownloaderOperation的优先级的

if (options & SDWebImageDownloaderHighPriority) {
    operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
    operation.queuePriority = NSOperationQueuePriorityLow;
}

通过SDWebImagemanager调用对应的是SDWebImageLowPrioritySDWebImageHighPriority这两个值。

  1. 渐进式加载SDWebImageDownloaderProgressiveDownload

如果设置了该选项,在- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data回调方法中就会处理图像数据,通过progressBlock返回图像,会有性能损耗,默认是关闭的。通过SDWebImagemanager调用对应的是SDWebImageProgressiveDownload值。

  1. SDWebImageDownloaderUseNSURLCache 使用URL缓存选项

如果设置看该选项,创建NSMutableURLRequest的时候cachePllicy会使用NSURLRequestUseProtocolCachePolicy,也就是会使用URL的缓存策略,否则使用NSURLRequestReloadIgnoringLocalCacheData忽略缓存。通过SDWebImagemanager调用对应的是SDWebImageRefreshCached这个值。

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
  1. SDWebImageDownloaderContinueInBackground 后台下载选项

参考之前提到的后台下载任务,默认是不开启的 ,通过SDWebImagemanager调用对应的是SDWebImageLowPrioritySDWebImageContinueInBackground这两个值。

  1. 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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!