SDWebImage源码解析(四)——SDWebImage图片下载模块

第四篇的写在前面

本篇文章为SDWebImage源码阅读解析的最后一篇文章,主要介绍SDWebImage的图片下载功能。主要涉及两个重要的类——SDWebImageDownloaderSDWebImageDownloaderOperation。在第一篇介绍的SDWebImageManager类中持有SDWebImageDownloader属性,通过loadImageWithURL()方法调用SDWebImageDownloader中的downloadImageWithURL()方法对网络图片进行下载。
本模块的设计设计NSURLSession的使用,如果对这个类不熟悉的话,可以参考官方开发者文档URL Session Programming Guide

//使用更新的downloaderOptions开启下载图片任务
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
      //do something...
            }];  
           

SDWebImageDownloader的设计

在SDWebImage中,SDWebImageDownloader被设计为一个单例,与SDImageCacheSDWebImageManagerd类似。

用于管理NSURLRequest对象请求头的封装、缓存、cookie的设置,加载选项的处理等功能。管理Operation之间的依赖关系。SDWebImageDownloaderOperation是一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。

初始化方法和相关变量

SDWebImageDownloader提供了一个全能初始化方法,在里面对一些属性和变量做了初始化工作:

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        _operationClass = [SDWebImageDownloaderOperation class];
        //默认需要对图片进行解压
        _shouldDecompressImages = YES;
        //默认的任务执行方式为FIFO队列
        _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
        _downloadQueue = [NSOperationQueue new];
        //默认最大并发任务的数目为6个
        _downloadQueue.maxConcurrentOperationCount = 6;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];
        //设置默认的HTTP请求头
#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;

        sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;

        /**
         *初始化session,delegateQueue设为nil因此session会创建一个串行任务队列来处理代理方法
        *和请求回调。
        */ 
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}

downloadImageWithURL()方法

downloadImageWithURL ()是下载器的核心方法。


/**
 *  通过创建异步下载器实例来根据url下载图片
 *
 *  当图片下载完成后或者有错误产生时将通知代理对象
 *
 *
 * @param url            The URL to the image to download
 * @param options        The options to be used for this download
 * @param progressBlock  当图片在下载时progressBlock会被反复调用以通知下载进度,该block在后台队列执行
 * @param completedBlock 图片下载完成后执行的回调block
 *
 * @return A token (SDWebImageDownloadToken) 返回的token可以被用在 -cancel 方法中取消任务
 */
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        
        __strong __typeof (wself) sself = wself;
        //1. 设置超时
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        //2. 关闭NSURLCache,防止重复缓存图片请求
        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
        //如果options中设置了使用NSURLCache则开启NSURLCache,默认关闭
        if (options & SDWebImageDownloaderUseNSURLCache) {
            if (options & SDWebImageDownloaderIgnoreCachedResponse) {
                cachePolicy = NSURLRequestReturnCacheDataDontLoad;
            } else {
                cachePolicy = NSURLRequestUseProtocolCachePolicy;
            }
        }
        //3. 初始化URLRequest
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        //4. 添加请求头
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        //5. 初始化operation 对象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        //6. 指定验证方式
        if (sself.urlCredential) {
            //SSL验证
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            //Basic验证
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //7. 将当前operation添加到下载队列
        [sself.downloadQueue addOperation:operation];
        
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            //为operation添加依赖关系      
            //模拟栈的数据结构 先进后出
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

downloadImageWithURL方法中,直接返回了一个名为addProgressCallback的方法,并将其中的代码保存到block中传给这个方法。

addProgressCallback方法

addProgressCallback方法主要用于设置一些回调并且保存,并且执行downloadImage中保存的代码将返回的operation添加到数组中保存。block保存的数据结构如下图:

SDURLCallBacks.png
/** 为callbackBlocks添加callbackBlock->callbackBlock中包含:completedBlock,progressBlock
 *
 * @return SDWebImageDownloadToken
 */
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    //URL用于给 callbakcs 字典 设置键,因此不能为nil
    //如果url == nil 直接执行completedBlock回调
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    // 初始化 token 其实就是一个标记
    __block SDWebImageDownloadToken *token = nil;
    // 在barrierQueue中同步执行
    dispatch_barrier_sync(self.barrierQueue, ^{
        //1. 根据url获取operation
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            //2. operation不存在
            //执行operationCallback回调的代码 初始化SDWebImageDownloaderOperation
            operation = createCallback();
            //3. 保存operation
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            //4. 保存完成的回调代码
           operation.completionBlock = ^{
              //下载完成后在字典中移除operation
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }
        //5. 保存将回调保存到operation中的callbackBlocks数组 注意这是属于operation的对象方法
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        //6. 设置token的属性
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

SDWebImageDownloaderOperation

Downloader部分的第二个类就是SDWebImageDownloaderOperation在上面的部分也已经用到过不少。它是NSOperation的子类,如果对NSOperation不熟悉的话,可以参考这篇文章
内部自定义了以下几个通知:

NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";

NSOperation进行自定义需要对以下几个方法进行重载:

/**
 * 重写NSOperation的start方法 在里面做处理
 */
- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        //1. 进行后台任务的处理
        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;
                }
            }];
        }
#endif
        //2. 初始化session
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            //如果Downloader没有传入session(self 对 unownedSession弱引用,因为默认该变量为downloader强引用)
            //使用defaultSessionConfiguration初始化session
            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.
             */
            /**
             * 初始化自己管理的session
             * delegate 设为 self 即需要自动实现代理方法对任务进行管理
             * delegateQueue设为nil, 因此session会创建一个串行的任务队列处理代理方法和回调
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        //使用request初始化dataTask
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    //开始执行网络请求
    [self.dataTask resume];

    if (self.dataTask) {
        //对callbacks中的每个progressBlock进行调用,并传入进度参数
        //#define NSURLResponseUnknownLength ((long long)-1)
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        //主队列通知SDWebImageDownloadStartNotification
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        //连接不成功
        //执行回调输出错误信息
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

#if SD_UIKIT
    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
}
/**
 * 重写NSOperation的cancel方法
 */
- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}
/**
* 内部方法cancel
*/
- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];

    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];
}
/**
* 重设operation
*/
- (void)reset {
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks removeAllObjects];
    });
    self.dataTask = nil;
    self.imageData = nil;
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

重点看start方法。NSOperation中需要执行的代码需要写在start方法中。

让一个NSOperation操作开始,你可以直接调用-start,或者将它添加到NSOperationQueue中,添加之后,它会在队列排到它以后自动执行。

类的内部定义了两个属性作为任务的标记:

@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;

需要注意到的是,如果我们在声明属性的时候使用了getter =的语义,则需要自己手动写getter,编译器不会帮我们合成。源码中手动声明了getter方法,并实现KVO。


- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

在初始化NSURLSession的时候,SDWebImageDownloaderOperation将自己声明为delegate

self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];

因此,需要实现NSURLSessionDelegate代理方法。具体的实现这里不赘述,主要是针对各种情况进行处理。但是需要注意到的是,在SDWebImageDownloader中同样遵守了NSURLSessionDelegate代理的方法,但是在downloader中只是简单的把operation数组中与task对应的operation取出,然后把对应的参数传入到SDWebImageDownloaderOperation中的对应的方法进行处理。例如:

// 接收到服务器的响应
- (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 *dataOperation = [self operationWithTask:dataTask];
    //将参数传入dataOperation中进行处理
    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}

/**
* 根据task取出operation
*/
- (SDWebImageDownloaderOperation *)operationWithTask:(NSURLSessionTask *)task {
    SDWebImageDownloaderOperation *returnOperation = nil;
    for (SDWebImageDownloaderOperation *operation in self.downloadQueue.operations) {
        if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
            returnOperation = operation;
            break;
        }
    }
    return returnOperation;
}

结尾

对于SDWebImage的主要功能在这四篇解析文章中大致的分析,除此以外的功能可能还需要对源码进行更深入的阅读分析才能有更深的了解。由于笔者水平有限,未免出现有分析不准确或者有不到位的地方,请见谅。
源码阅读是一个需要耐心的过程,尽管在途中会遇到一些困难,但是只要多查资料多思考,就会有收获。

参考文献:

  1. SDWebImage源码阅读
  2. iOS中使用像素位图(CGImageRef)对图片进行处理
  3. 一张图片引发的深思
  4. SDWebImage源码解读_之SDWebImageDecoder
  5. Difference between [UIImage imageNamed…] and [UIImage imageWithData…]?
  6. How-is-SDWebImage-better-than-X?
  7. Understanding SDWebImage - Decompression
  8. why decode image after [UIImage initwithData:] ?
  9. Image Resizing Techniques
  10. 多线程之NSOperation简介
  11. SDWebImage源码阅读笔记
  12. URL Session Programming Guide
  13. SDWebImage源码解析
  14. Quartz 2D Programming Guide
  15. Avoiding Image Decompression Sickness
  16. NSOperation
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容