【源码解读】SDWebImage ─── 下载器的设计

一. 下载器的介绍

下载器在SDWebImage中和缓存是相辅相成的(关于它们的合作要在 <SDWebImage ─── 总结>才会说明)。下载器(其实用下载操作生成器来形容比较贴切)提供这样一个功能:根据提供的参数生成一个下载操作,把下载操作返回给你,并且下载中或完成时会通过Block回调给你。简单点说,你给我一个url,我创建一个操作去下载。

SDWebImage的下载器功能主要有两个类组成。

SDWebImageDownloader;//管理类,管理所有的下载操作
SDWebImageDownloaderOperation;//继承NSOperation的操作类,主要用来下载

通过该篇下载器的学习,你可以学到如何设计一个能同时进行多个下载操作的下载器,也可以更加了解多线程和网络这块的知识点。可能之前都是用AFNetWorking实现下载功能,而忽视了最基础的NSURLSession。

二. 下载器的设计

说起下载器的设计,我们主要从三个方面来说:
① 下载操作管理类的设计(SDWebImageDownloader)
② 下载操作类的设计(SDWebImageDownloaderOperation)

这两个的职责其实很明确,我们知道SDWebImageDownloaderOperation是继承NSOperation类,而它主要是封装了下载的处理的内容,才会被称为下载操作类。而SDWebImageDownloader是用来管理这些下载操作类的,因为多个下载时也会有多个下载操作,所以这边要由下载操作管理类来统一管理。

① 下载操作管理类的设计

要谈到下载操作管理类的设计,我们就要先清楚管理类作用就是管理所有的下载操作,让多个下载操作能各司其职,互不干扰。同时,能够设置一些下载所需要的网络属性,比如请求头,会话等等。

我们通过SDWebImageDownloader暴露的属性和API就可以发现都是围绕着下载网络相关设置和下载操作管理这两点来进行的(当然,本质都是操作SDWebImageDownloader内部的私有属性,比如downloadQueue、HTTPHeaders、session)。

  • 用来设置自定义证书
  • 用来设置请求头
  • 设置队列属性(最大并行下载数量,超时时间,下载顺序)
  • 用来管理下载操作(下载,暂停,取消)
/*     用来设置下载后要不要立即解压图片    */
@property (assign, nonatomic) BOOL shouldDecompressImages;

/*     用来设置队列属性    */
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

/*    用来设置自定义证书    */
@property (strong, nonatomic) NSURLCredential *urlCredential;
@property (strong, nonatomic) NSString *username;
@property (strong, nonatomic) NSString *password;

/*   用来设置请求头     */
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
- (NSString *)valueForHTTPHeaderField:(NSString *)field;

/*   用来管理下载操作   */
- (void)setOperationClass:(Class)operationClass;
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (void)setSuspended:(BOOL)suspended;
- (void)cancelAllDownloads;

其中我们主要用到下载的方法,也是下载管理类的核心API:给我提供对应的参数,给你创建一个下载操作,添加到我的下载队列中

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

我们可以看出创建一个下载队列需要这么几个参数:

  • url(下载地址)
  • options(下载选项,如何去下载以及下载后怎么做)
  • progressBlock(下载过程中的回调)
  • completedBlock(下载完成后的回调)

其中options给我们提供了很多类型的模式:

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    //这个属于默认的使用模式了,前往下载,返回进度block信息,完成时调用completedBlock
    SDWebImageDownloaderLowPriority = 1 << 0,
    //渐进式下载 ,如果设置了这个选项,会在下载过程中,每次接收到一段返回数据就会调用一次完成回调,回调中的image参数为未下载完成的部分图像,可以实现将图片一点点显示出来的功能
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    //使用NSURLCache
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    //如果从NSURLCache中读取图片,会在调用完成block的时候,传递空的image或者imageData
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    //系统为iOS 4+时候,如果应用进入后台,继续下载
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    //存储在NSHTTPCookieStore的cookies
    SDWebImageDownloaderHandleCookies = 1 << 5,
    //允许不受信任的SSL证书
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    //将图片下载放到高优先级队列中
    SDWebImageDownloaderHighPriority = 1 << 7,
};

大概了解了整个管理类的功能属性,我们就从内部实现一步步了解起:

(1)管理类的初始化和属性设置

这种管理类的初始化也比较简单,就是单例嘛,因为既然用来管理所有的下载操作,就必须是唯一的,总不能创建一个下载操作就有一个管理类吧。

下载操作管理类SDWebImageDownloader会通过sharedDownloader创建一个单例对象,并且在init方法里面初始化一些属性和网络设置(主要有下载操作队列、下载会话session、请求头的设置以及其他属性的初始化),当然也可以通过一些对外的属性来更改这些设置。

+ (SDWebImageDownloader *)sharedDownloader {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (id)init {
    if ((self = [super init])) {
        //这个属性的存在主要用来让我们自定义一个下载操作类来替换
        _operationClass = [SDWebImageDownloaderOperation class];
        _shouldDecompressImages = YES;
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;

        //下载操作队列,用来管理下载操作
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";

        //保存所有操作队列的progressBlock和completedBlock
        _URLCallbacks = [NSMutableDictionary new];

        //设置请求头
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];//请求头
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif

       //栅栏队列,用来起到加锁的作用
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
        _downloadTimeout = 15.0;

        //设置配置
        NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
        sessionConfig.timeoutIntervalForRequest = _downloadTimeout;

        //delegateQueue设置为nil,session就会创建一个串行队列来执行所有的代理方法
        self.session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}
(2)管理类如何管理

之前我们提到的管理类的作用就是管理所有的下载操作,让多个下载操作能各司其职,互不干扰。那它是怎么实现的呢?

  • 通过队列downloadQueue来管理下载操作
    通过将下载操作添加到我们的下载队列中,我们可以不仅可以对队列进行整体的操作(暂停,取消),也可以通过taskIdentifier找到对应的下载操作进行单独操作。

  • 通过字典URLCallbacks来保存每一个操作的progressBlock和completedBlock
    当有多个操作时,就会有多个progressBlock和completedBlock,所以就需要用一个集合类来保存这些代码块,以便在某个操作能够找到并执行自己的progressBlock和completedBlock。
    这边字典URLCallbacks的结构是以url字符串为key,value是progressBlock和completedBlock组成的字典。例子如下

{
   "https://ss0uperman/img/c.png":{
                                    "progress":progressBlock,
                                    "completed":completedBlock
                                    }
}

具体的代码如下:

//创建下载操作
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;

    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        //设置超时时间
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // 为了防止重复的缓存 (NSURLCache + SDImageCache) 。如果有NSURLCache,就要禁止自己的SDImageCache
        // 设置是SDWebImageDownloaderUseNSURLCache(使用NSURLCache),是将request的cachePolicy设置成按照协议的缓存策略来定,不然就是忽略本地缓存
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        //设置是否自动发送cookie
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        //形成通道,不比等到响应就可发送下一条请求,也实现TCP的复用
        request.HTTPShouldUsePipelining = YES;
        
        //设置请求头
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        
        //创建下载操作
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                             //下载中部分
                                                             SDWebImageDownloader *sself = wself;
                                                             if (!sself) return;
                                                             __block NSArray *callbacksForURL;
                                                             dispatch_sync(sself.barrierQueue, ^{
                                                                 callbacksForURL = [sself.URLCallbacks[url] copy];
                                                             });
                                                             for (NSDictionary *callbacks in callbacksForURL) {
                                                                 dispatch_async(dispatch_get_main_queue(), ^{
                                                                     SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                                                                     if (callback) callback(receivedSize, expectedSize);
                                                                 });
                                                             }
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                            //完成部分
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            __block NSArray *callbacksForURL;
                                                            dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });
                                                            for (NSDictionary *callbacks in callbacksForURL) {
                                                                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                                                                if (callback) callback(image, data, error, finished);
                                                            }
                                                        }
                                                        cancelled:^{
                                                            //取消部分
                                                            SDWebImageDownloader *sself = wself;
                                                            if (!sself) return;
                                                            dispatch_barrier_async(sself.barrierQueue, ^{
                                                                [sself.URLCallbacks removeObjectForKey:url];
                                                            });
                                                        }];
        //设置是否解压图片
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        
        //身份认证
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        //设置下载操作的优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        //添加到下载队列中
        [wself.downloadQueue addOperation:operation];
        //设置队列的下载顺序(其实就是设置依赖关系)
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];
    return operation;
}

//将每个下载操作的progressBlock,completedBlock保存起来
//因为是同时有多个下载操作,所以要保持起来,到时候任务完成根据url去找出来调用
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    //如果url为空,不用保存操作的回调,也不用下载,马上completedBlock返回没有图片数据
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    //栅栏函数(里面的代码类似于一个加锁的作用),防止多个线程同时对URLCallbacks进行操作
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        
        // 将每个下载操作的progressBlock,completedBlock保存起来
        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;

        //第一次下载该url,才有后续操作(比如保存回调,下载),不是第一次就不用了
        if (first) {
            createCallback();
        }
    });
}

其中比较新奇的是-
addProgressCallback:completedBlock:forURL:createCallback:
的写法,看起来挺奇怪的,其实就是将将每个下载操作的progressBlock,completedBlock保存在URLCallbacks中,并且做一个校验,看是不是第一次下载,是第一次下载才需要通过createCallback回调继续执行。

另外,通过创建一个下载操作,并且把会话session和请求request传给operation主要是为了让所有的下载操作共用同一个会话。在该管理类中,遵守了NSURLSessionTaskDelegate, 和NSURLSessionDataDelegate协议,当作为代理的自己执行代理方法时,会将代理方法分配给各自的操作类,让它们自己去处理下载的协议。管理类只需要获取操作类处理好的数据就可以了。

② 下载操作类的设计
(1)NSOperation的认识

讲解完下载操作管理类的设计,肯定对下载操作类的内部的实现有所好奇(其实主要是它怎么处理获取的数据)。我们知道下载操作类SDWebImageDownloaderOperation继承于NSOperation,在讲解之前我们先来了解一下NSOperation。

NSOperation默认是非并发的, 也就说如果你把operation放到某个线程执行, 它会一直block住该线程, 直到operation finished. 对于非并发的operation你只需要继承NSOperation, 然后重写main()方法就可以了。但是我们需要的是并发NSOperation。所以我们需要:

  • 重写isConcurrent函数, 返回YES
  • 重写start()函数
  • 重写isExecuting和isFinished函数

NSOperation有三个状态量isCancelled, isExecuting和isFinished。非并发的话,main函数执行完成后, isExecuting会被置为NO, 而isFinished则被置为YES。而并发的话,因为task是异步执行的,系统不知道operation什么时候finished,所以需要你手动去管理。
当这个操作类和下载关联起来时,我们就在start()函数中开启网络下载,并设置isExecuting,在网络完成回调中设置isFinished。这样我们就掌握了这个操作类的生命周期。

(2)NSOperation的子类SDWebImageDownloaderOperation

接下来回到我们的下载操作类SDWebImageDownloaderOperation,SDWebImageDownloaderOperation覆写了父类的这executing和finished两个只读属性,让他们变成可读写的。

@property (assign, nonatomic, getter = isExecuting) BOOL executing;//是否正在执行
@property (assign, nonatomic, getter = isFinished) BOOL finished;//是否完成

SDWebImageDownloaderOperation实际上包含着一个task(NSURLSessionTask类型),当操作开始时(start()执行),开始下载,当执行下载完成的代理方法时或者请求错误时,设置isFinished,也意味着操作的完成。

(3)SDWebImageDownloaderOperation的状态(开始、取消,结束)

当操作开始执行时,start()函数开始执行,start函数中主要做了以下几件事:

  • 判断isCancelled的值,若取消了,就重置属性,不用继续下载
  • 申请一段后台时间来进行下载(如果后台时间快到,也会将下载停掉)
  • 创建会话session,创建一个task请求进行下载,需要注意的是这边的会话可能是自身创建的或者是初始化时从管理类传进来的。
  • 设置executing的值

以上是在给self加锁的情况下进行的

  • 判断task是否存在,然后进行对应的通知和回调
  • 再次确保后台任务标识符销毁(这一步不太清楚其含义啊?)
//操作开始执行
- (void)start {
    @synchronized (self) {
        //判断是否已经取消
        if (self.isCancelled) {
            self.finished = YES;
            //重置清空
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        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)];
            //申请更长的后台时间来完成下载,时间快到时会执行block中的代码
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                // 当应用程序留给后台的时间快要结束时(该时间有限),这个block将执行:进行一些清理工作(主线程执行),清理失败会导致程序挂掉
                if (sself) {
                    //主要是用来取消下载操作
                    [sself cancel];

                    //endBackgroundTask:和beginBackgroundTaskWithExpirationHandler成对出来,意思是结束后台任务
                    // 标记指定的后台任务完成
                    [app endBackgroundTask:sself.backgroundTaskId];
                    // 销毁后台任务标识符
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        //创建会话session
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  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.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        //创建task请求
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
        self.thread = [NSThread currentThread];
        
    }
    
    //开始下载任务
    [self.dataTask resume];

    //如果task请求存在
    if (self.dataTask) {
        //下载刚开始,接收大小(0)和总预计接收大小未知(-1)
        if (self.progressBlock) {
            self.progressBlock(0, NSURLResponseUnknownLength);
        }
        //主线程回调下载开始的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    }
    else {//task请求不存在
        //直接在completedBlock回调错误信息
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
        }
    }

    // 确保后台任务标识符销毁
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    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;
    }
#endif
}

取消的话都会执行到cancelInternal函数,取消内部task,并且进行对应回调,重设状态值。

//取消的内部操作
- (void)cancelInternal {
    //如果已完成就不用取消了
    if (self.isFinished) return;
    [super cancel];
    //如果有取消后的代码块,就执行
    if (self.cancelBlock) self.cancelBlock();

    if (self.dataTask) {
        [self.dataTask cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }
    [self reset];
}

至于结束有两种情况,当执行下载完成的代理方法时或者请求错误时,主要都在会话session的代理方法中执行,可以通过下面下载过程的讲解来认识。

(3)SDWebImageDownloaderOperation下载过程

为什么讲下载过程呢?因为其实SDWebImageDownloaderOperation把下载封装起来,一个操作对应一个下载,下载过程对数据的处理都是在SDWebImageDownloaderOperation中。
SDWebImageDownloaderOperation遵守了NSURLSessionTaskDelegate, 和NSURLSessionDataDelegate协议。
当session的值不为nil,意味着用管理类传进来共用的会话session,当session的值为nil,就需要在操作类中创建一个私有的会话session。不管是用谁的session,都会实现NSURLSession的几个代理方法。

NSURLSessionDataDelegate主要实现了三个方法:

  • dataTask收到响应的代理方法。(告诉delegate已经接收到服务器的初始应答, 接下来准备数据任务的操作)

在接收到服务器的响应时,通过判断响应的状态码和期望收到的内容大小来判断是否有数据。
有数据的情况就直接保存一些相关的参数,比如期望的数据大小,响应。没数据的情况又分成请求失败和304(没有更新),这两种都是会取消下载。

// 告诉delegate已经接收到服务器的初始应答, 接下来准备数据任务的操作
// 在这里判断响应,来看是否有数据下载
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //'304 Not Modified' is an exceptional one
    // HTTP状态码:200+正常成功的;300+重定向;400+请求错误;500+一般时服务端的问题;304 — 未修改,文档没有改变。
    // 意思是:小于400除了304都是请求成功,有数据
    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;
        if (self.progressBlock) {
            self.progressBlock(0, expected);
        }
        
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        //在主线程回调收到响应的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {//没有数据的情况下(一个是304,一个是请求错误)
        NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
        
        //304意味着数据没有改变,我们要先取消再从缓存中获取
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.dataTask cancel];
        }
        //在主线程回调停止下载的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
        
        if (self.completedBlock) {
            self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
        }
        [self done];
    }
    
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}
  • dataTask接收到数据的代理方法

收到数据一个默认的操作是将收到的数据保存起来,通过progressBlock传出所受到的数据长度和期望收到的总长度。
如果是你是渐进式下载的话(SDWebImageDownloaderProgressiveDownload),就需要把目前收到的data转成image,然后通过completedBlock将image回传出去。所以说completedBlock不是只单单传最后完成的回调。

//接收到数据的回调(可能回调多次)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    //拼接到imageData中
    [self.imageData appendData:data];

    //如果是渐进式的设置 && 可以收到数据
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {

        // 下载数据的总大小
        const NSInteger totalSize = self.imageData.length;

        // Update the data source, we must pass ALL the data, not just the new bytes
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);

        // width + height == 0意味着还没数据(第一次接收到数据)
        if (width + height == 0) {
            //获取图片资源的属性字典
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                //获取高度
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                //赋值给height
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                //获取宽度
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                //赋值给width
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                //获取方向
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                //赋值给orientationValue
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                //释放属性字典
                CFRelease(properties);

                // When we draw to Core Graphics, we lose orientation information,
                // which means the image below born of initWithCGIImage will be
                // oriented incorrectly sometimes. (Unlike the image born of initWithData
                // in didCompleteWithError.) So save it here and pass it on later.
                
                //当我们画到Core Graphics(initWithCGIImage),我们会失去我们的方向信息,所以我们要保存起来
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }

        // 接收到数据并且还没接收完所有的
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image(局部数据)
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // 解决iOS平台图片失真问题
            // 因为如果下载的图片是非png格式,图片会出现失真
            // 为了解决这个问题,先将图片在bitmap的context下渲染
            if (partialImageRef) {
                //局部数据的高度
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                // 创建rgb空间
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                // 获取上下文 bmContext
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    //绘制图片到context中
                    //这里的高度为partialHeight  因为height只在宽高都等于0的时候才进行的赋值,所以以后的情况下partialHeight都等于0,所以要使用当前数据(imageData)转化的图片的高度,partialImageRef为要绘制的image
                    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
            if (partialImageRef) {
                //转化成UIImage
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                //获取图片的key,其实就是url
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 对图片进行处理(使用@2x或@3x)
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                //解压图片
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                //主线程从completedBlock将image回传出去(这边只传出image,是还未完成的)
                dispatch_main_sync_safe(^{
                    if (self.completedBlock) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }

        CFRelease(imageSource);
    }

    //传出progressBlock(只要执行这个方法都会传出)
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}
  • dataTask将要缓存响应的代理方法
// 告诉delegate是否把response存储到缓存中
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    //如果这个方法执行,意味着响应不会从缓存获取
    responseFromCached = NO;
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // 防止缓存响应
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

NSURLSessionTaskDelegate主要实现了两个方法:

  • task完成执行的代理方法

下载完成会发出通知(有错误的情况值是Stop,没错误才会发出Stop和Finished)
另外有错误的情况下,只需要回调错误信息。没错误的情况的情况下还得判断是否有imageData,有的话将imageData转成image,然后进行对应操作(变成对应大小,解压),再通过completionBlock传出去。
执行完上面的步骤后,将completionBlock置为nil,以免造成开发者的循环引用。并且将相关的属性重置清空。

//下载完成的回调
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    @synchronized(self) {
        self.thread = nil;
        self.dataTask = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            //发出停止的通知
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            if (!error) {
                //如果没有error则再发出完成的通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            }
        });
    }
    //如果有error,就没返回数据,只返回error和finish的标示
    if (error) {
        if (self.completedBlock) {
            self.completedBlock(nil, nil, error, YES);
        }
    } else {
        SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
        
        if (completionBlock) {
            /**
             *  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 
             */
            //设置是忽略缓存响应 && 从缓存获取响应 && url缓存中找得到self.request的缓存
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                completionBlock(nil, nil, nil, YES);
            } else if (self.imageData) {//有imageData
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                //通过url的判断(包含@2x.@3x.)将图片转换成对应大小
                image = [self scaledImageForKey:key image:image];
                
                // 只解压图片,不解压gif
                if (!image.images) {
                    if (self.shouldDecompressImages) {
                        image = [UIImage decodedImageWithImage:image];
                    }
                }
                //解压的图片的size等于0
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
                }
                else {//解压的图片的size正常
                    completionBlock(image, self.imageData, nil, YES);
                }
            } else {//没有imageData
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
            }
        }
    }
    
    self.completionBlock = nil;
    [self done];
}
  • task收到挑战执行的代理方法
//告诉delegate, task已收到授权:处理服务器返回的证书, 需要在该方法中告诉系统是否需要安装服务器返回的证书
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    
    // 判断服务器返回的证书是否是服务器信任的
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        //如果设置不是忽略非法SSL证书
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            //使用默认处置方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            //使用自签证书
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        //之前验证失败次数等于0
        if ([challenge previousFailureCount] == 0) {
            if (self.credential) {
                //使用自签证书
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                //取消证书验证
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            //取消证书验证
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    
    //安装证书
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容