源码启示录 | 如何使用NSURLSession下载海量图片

网络相册

无论是社交类的网络相册、电商类的商品清单、还是电子书城的书架等应用场景,大量图片的下载都是必备的应用需求。
在iOS系统中,相比 NSURLConnection ,NSURLSession 提供了一套更优秀的网络处理解决方案,并且使用的接口更加简单,对开发者更加友好。那么 NSURLSession 能胜任多任务下载吗?

NSURLSession 多任务的局限性

NSURLSession 本身能处理多任务下载, 我们可以使用dataTaskWithURL方法为每个URL发起请求:

NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl]
          completionHandler:^(NSData *data,
                              NSURLResponse *response,
                              NSError *error) {
            NSLog(@"Handle response"); // 断点位置

  }] resume];
NSURLSession的下载队列

可见,NSURLSession 内部开辟NSOperationQueue 来处理多任务,NSURLSession 的 block 使用简单,但能实现的功能有限,所以,要想控制下载的过程,就必须使用代理:

// NSURLSessionDataDelegate
- URLSession:dataTask:didReceiveResponse:completionHandler: //接收到服务器的响应
- URLSession:dataTask:didReceiveData: //接收到服务器的数据
- URLSession:dataTask:willCacheResponse:completionHandler: //指定缓存数据

// NSURLSessionDataDelegate
- URLSession:task:didCompleteWithError: //下载完成
- URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: //重定向

然而,对一个NSURLSession而言,所有任务都会走同一个代理方法,导致代理中需要编写许多区分不同任务的代码,为防止公共属性访问冲突,需要对处理过程加锁,这导致了性能的下降;
当然,有人会说,每个任务用不同的 NSURLSession 就好了,诚然,这可以解决代理复用的问题,但正如前面提到的,每个NSURLSession自己会维护一个NSOperationQueue,这种处理方法相当于为每个下载任务开辟了一个NSOperationQueue,却只放入一个操作对象,不但代码非常别扭,在大量任务时会增加系统的开销。

那么,该如何有效地将任务分开,分别控制下载进程,优雅的处理多任务下载呢?

SDWebImage的解决方法

SDWebImage 作为GitHub 获得Star 20K+ 的优秀库,在实现了大量图片异步下载、缓存、解码一系列功能的同时,提供了简洁的使用接口,达到了优异的性能,它内部也是使用NSURLSession下载图片的,那么它是如何控制NSURLSession的呢?让我们到SDWebImage的源码中一探究竟。

下载相关的类

SDWebImage 中负责下载的主要有两个类:

  • SDWebImageDownloader 封装了 NSURLSession ,同时作为 Session 的代理,并初始化了一个 NSOperationQueue ,管理所有的下载过程。
  • SDWebImageDownloaderOperation 实现了一张图片的整个下载过程,通过 task 启动下载任务,开辟新线程处理下载到的图片等等。

它们的关系如下图,实心箭头表示持有关系,虚线表示间接使用:


SDWebImage下载主要类

下载的主要过程解析

下载入口

下载的过程首先从 Downloader 的 downloadImage 方法开始:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock

若URL未被下载过,一个 SDWebImageDownloaderOperation 就会被创建,并添加到 downloadQueue 中,由队列负责执行,这时,还没实际下载,只有Operation 被提交执行,才会开始真正的下载。

启动下载

接着来到 SDWebImageDownloaderOperation 中看看下载的启动 :
下载的启动过程放在了 start 方法中,而不是 main 方法,这是因为main方法执行完成后,会自行退出,无法控制线程的生命周期。
在 star 方法中,一个 NSURLSessionTask 实例被创建,并通过以下方法启动起来:

[self.dataTask resume];

这时,实际的下载过程是在NSURLSession的队列中进行的,SDWebImageDownloaderOperation 的线程处于【等待】的状态中。

下载完成

下载完成后,在 SDWebImageDownloader 中获得NSURLSessionTaskDelegate 回调:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

在该代理方法中,通过遍历 self.downloadQueue.operations 查询 NSURLSessionTask 对应的 SDWebImageDownloaderOperation,调用 Operation 实现的同名代理方法URLSession:dataTask:didReceiveResponse:completionHandler:,将执行的过程切回到 Operation 中,在这里完成最后的收尾工作,调用

[self done];

需要注意的是,以下属性的 setter 方法:

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

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

自定义实现并发操作对象时,必须覆盖这些属性的实现,以便可以返回操作的状态,并且必须为这些键路径生成KVO通知(即setter中的willChangeValueForKeydidChangeValueForKey),否则会造成 NSOperation 无法退出。

到这里就将一张图片下载的主要过程分析完了,其他网络错误处理、缓存处理过程,可以沿着这个流程进一步加以分析,这里限于篇幅就不展开了。

NSOperation vs GCD

这里补充一点的是,在 Operation 过程中,切到主线程发通知、调用解码器等,都使用GCD开辟新线程,从而释放出 NSURLSession 的下载线程,这是比较有意思的部分,NSOperation 内部的具体实现是用GCD的,体现了NSOperation 和 GCD 的区别与联系:

  • NSOperation 用于控制复杂过程的生命周期;
  • GCD 用于快速执行异步操作;
  • 两者都是启用多线程的手段,可以协同工作。

总结

SDWebImage的下载方案中的优点:

  • 异步下载图片,开辟多个线程处理各阶段过程,使主线程不卡顿
  • 灵活使用 NSOperation 和 GCD,使得代码条理清晰
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容