# 前言
最近开始做了一个新项目,几乎没有时间来写自己的博客,大部分都在写 feature(BUG),自己研究的东西很少,本来之前说好每个月要写两篇文章也没能坚持下来,最近在项目中遇到了一些问题,就在这里总结下吧。一些小的技巧而已,大神可以忽略了。
# 背景
新项目包含了上传下载网络请求相关功能,由于是 swift 编写所以自然而然选择了 AlamoFire (好像也没得选) 来做底层,正常的网络请求 post、get 等都是直接傻瓜式调用 AlamoFire 的接口,本文主要将一些细节问题
# 设置通用超时时间
使用 Alamofire 发起请求时候有这两个接口
1 | /// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`, |
而我们在调用的时候通常会直接这么用
1 | let req : URLRequest = URLRequest(url: URL(fileURLWithPath: "32"), cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) |
其中第一种方法我们不能传入超时时间,第二中方法我们可以通过传入的 URLRequest 来设置超时时间,但是我们通常一个项目中大部分的请求,可能除了某些特殊的下载请求之外所有的超时时间都是一样的,这样的话我们需要同样的代码写好多遍,这个时候有两个办法
对生成 Request 的方法做一个封装,通用的参数如超时时间、header、请求方式 写死在方法里面,对于会变动的参数如 URL 和可以通过参数传入.
创建
Alamofire.SessionManager
通过 sessionManager 来设置超时时间等一些通用的东西
1 | let networkManager : SessionManager = { |
# 断点续传
Alamofire 支持断点续传下载,原理就是将下载一半的数据保存到本地,然后下次再启动时候通过 data 的拼接来进行继续下载。用法也很简单,只是调用接口而已,关键是看开发者如何自己去维护这个已下载的数据,比如是存内存还是存硬盘,要存多久,淘汰策略是什么之类的。其实就是两个步骤, 断点和续传
# 第一步 断点
监听下载中断,中断后将已经下载的数据进行保留,我这边用一个属性来存,具体到项目实现大家可以采用自己存储方式,存到硬盘或者数据库之类的
1 | Alamofire.download("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4", method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in |
# 第二步 续传
当下载再次启动时候,需要在上一步数据的基础上继续下载,我们调用 Alamofire 这个方法
1 | /// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a |
这个接口需要我们传入已存在的数据,然后基于我们传入的数据进行下载,它支持从新指定目的地路径,如果你有需要可以重新指定
1 | Alamofire.download(resumingWith: tmpData!) |
同样他返回一个 request 的对象,我们可以通过点语法来拿到进度、response 等信息
# 批量下载
当我们需要同时下载很多东西的时候,往往需要我们自己维护一个下载队列,比如下一个载素材列表之类的。Alamo 给我们提供了下载的接口,但是下载的线程队列需要我们自己去维护,其实就是一个多线程并发队列。
# GCD
我们很自然而然的想到 GCD,但是 GCD 有一个问题无法控制最大并发数,而且对队列的管理也并不完善,比如我们要下载 100 个文件,如果同时下载的话开辟 100 个线程,那肯定是不行的,先不说移动设备是否支持 (最多 70 个左右),即使支持了那这个开销太大。虽说 GCD 的话可以使用信号量进行线程控制,但是每个线程的暂停启动之类的又是问题,而且毕竟是曲线救国的方法。
# OperationQueue
Operation 及 OperationQueue 是基于 GCD 封装的对象,作为对象可以提供更多操作选择,可以用方法或 block 实现多线程任务,同时也可以利用继承、类别等进行一些其他操作;但同时实现代码相对复杂一些。但是他毕竟不像 GCD 那样使用 C 语言实现,所以效率会相比 GCD 低一些。但是对线程的控制的灵活性要远高于 GCD,对于下载线程来说可以优先选择这个。
# 实现
我们把每一个下载任务封装成一个 operation。注意 Operation 不能直接使用,我们需要使用他的子类,这里我选择使用 BlockOperation
他的闭包则是需要执行的下载任务,然后我们把他添加进 queue 中便开始执行了任务
1 | let op : BlockOperation = BlockOperation { [weak self] in |
每一个 opeeation 对象我们都可以设置他的优先级、启动、暂停、等属性,简单的调用接口就可以,在此就不一一作解释了。然后我们需要对我们的 queue 进行设置,我们设置最大并发数,大家可以根据实际情况来设置,demo 中我只有两个下载任务,所以我就设置最大并发数为 1 这样就是一个一个下载。
1 | let queue : OperationQueue = { |
我们运行然后点击开始下载
很奇怪我们发现他还是同时下载,我们又试了其他的个数,无论多少都是同时下载,最大线程数量完全不起作用,再反过来看下上面加入 queue 的任务。正常来说每一个 operation 都要等上一个 operation 完成后才会执行,而系统判断完成的标准就是上一个 operation 的闭包走完,我们闭包中放入的是一个下载任务,而 Alamofire 的下载都是异步执行,所以导致 operation 的闭包走完了,但是其实下载是异步在另一个线程执行的,实际上下载没有完成,知道原因我们对症下药,只需要保证 operation 闭包中的代码是同步执行的就 OK 了。而 Alamofire 是基于 URLSession 来实现的,并没有像 connection 那样提供同步的方法,所以我们使用信号量卡一下,像这样
这样之后就会按照我们设置好的队列进行了
有人会说下载同步进行会不会有影响,其实不会的首先我们实现同步的方式是信号量,本质上还是异步的只是我们阻塞的当前的下载线程,这个被阻塞线程一定不是主线程 (除非 Alamofire 的开发者把他回调到主线程下载,这个基本不可能),而且当我们把这个下载任务加到一个 operation 中之后,就注定不会在主线程中了,没一个 operation 都会被系统分配到一个非主线程的地方去做,所以这样不会性能有任何影响。
# 总结
因为时间紧迫,暂时做了这么多,也遇到了这些问题,所以写出了总结下,本文还会继续更新,会慢慢的整个网络层分享出来。就是可能更新会慢,毕竟工作量有点饱和。多谢关注