AlamoFire的使用(下载队列,断点续传)

原文是我的博客 在这里 好多外链图片不显示 可以去我的博客查看

前言

最近开始做了一个新项目,几乎没有时间来写自己的博客,大部分都在写feature(BUG),自己研究的东西很少,本来之前说好每个月要写两篇文章也没能坚持下来,最近在项目中遇到了一些问题,就在这里总结下吧。一些小的技巧而已,大神可以忽略了。

image

背景

新项目包含了上传下载网络请求相关功能,由于是swift编写所以自然而然选择了 AlamoFire (好像也没得选)来做底层,正常的网络请求post、get等都是直接傻瓜式调用 AlamoFire 的接口,本文主要将一些细节问题

设置通用超时时间

使用Alamofire发起请求时候有这两个接口

/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of the specified `url`,
/// `method`, `parameters`, `encoding` and `headers`.
///
/// - parameter url:        The URL.
/// - parameter method:     The HTTP method. `.get` by default.
/// - parameter parameters: The parameters. `nil` by default.
/// - parameter encoding:   The parameter encoding. `URLEncoding.default` by default.
/// - parameter headers:    The HTTP headers. `nil` by default.
///
/// - returns: The created `DataRequest`.
public func request(_ url: URLConvertible, method: Alamofire.HTTPMethod = default, parameters: Parameters? = default, encoding: ParameterEncoding = default, headers: HTTPHeaders? = default) -> Alamofire.DataRequest

/// Creates a `DataRequest` using the default `SessionManager` to retrieve the contents of a URL based on the
/// specified `urlRequest`.
///
/// - parameter urlRequest: The URL request
///
/// - returns: The created `DataRequest`.
public func request(_ urlRequest: URLRequestConvertible) -> Alamofire.DataRequest

而我们在调用的时候通常会直接这么用

let req : URLRequest = URLRequest(url: URL(fileURLWithPath: "32"), cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)

        // 第一种方法调用,后面参数直接用default
        Alamofire.request(URL(fileURLWithPath: "32"))

        // 第二中调用,使传入request
        Alamofire.request(req)
        let semaphore = DispatchSemaphore(value: 0)

其中第一种方法我们不能传入超时时间,第二中方法我们可以通过传入的URLRequest来设置超时时间,但是我们通常一个项目中大部分的请求,可能除了某些特殊的下载请求之外所有的超时时间都是一样的,这样的话我们需要同样的代码写好多遍,这个时候有两个办法

  • 对生成Request的方法做一个封装,通用的参数如超时时间、header、请求方式 写死在方法里面,对于会变动的参数如 URL 和可以通过参数传入.

  • 创建 Alamofire.SessionManager 通过sessionManager来设置超时时间等一些通用的东西

let networkManager : SessionManager = {
        let config : URLSessionConfiguration = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 10
        let manager = Alamofire.SessionManager.init(configuration: config)
        return manager
    }()

断点续传

Alamofire支持断点续传下载,原理就是将下载一半的数据保存到本地,然后下次再启动时候通过data的拼接来进行继续下载。用法也很简单,只是调用接口而已,关键是看开发者如何自己去维护这个已下载的数据,比如是存内存还是存硬盘,要存多久,淘汰策略是什么之类的。其实就是两个步骤, 断点和续传

第一步 断点

监听下载中断,中断后将已经下载的数据进行保留,我这边用一个属性来存,具体到项目实现大家可以采用自己存储方式,存到硬盘或者数据库之类的

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
            return (URL(fileURLWithPath: String(describing : NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, false)[0]+"123.mp4")), [.createIntermediateDirectories, .removePreviousFile])
            }.responseJSON { (response) in

                switch response.result {

                case .success:
                    print("success")
                case .failure:
                    //意外中断后在此处处理下载完成的部分
                    self.tmpData = response.resumeData

                default:
                    print("failed")
                }

        }

第二步 续传

当下载再次启动时候,需要在上一步数据的基础上继续下载,我们调用Alamofire这个方法

/// Creates a `DownloadRequest` using the default `SessionManager` from the `resumeData` produced from a
/// previous request cancellation to retrieve the contents of the original request and save them to the `destination`.
///
/// If `destination` is not specified, the contents will remain in the temporary location determined by the
/// underlying URL session.
///
/// On the latest release of all the Apple platforms (iOS 10, macOS 10.12, tvOS 10, watchOS 3), `resumeData` is broken
/// on background URL session configurations. There's an underlying bug in the `resumeData` generation logic where the
/// data is written incorrectly and will always fail to resume the download. For more information about the bug and
/// possible workarounds, please refer to the following Stack Overflow post:
///
///    - http://stackoverflow.com/a/39347461/1342462
///
/// - parameter resumeData:  The resume data. This is an opaque data blob produced by `URLSessionDownloadTask`
///                          when a task is cancelled. See `URLSession -downloadTask(withResumeData:)` for additional
///                          information.
/// - parameter destination: The closure used to determine the destination of the downloaded file. `nil` by default.
///
/// - returns: The created `DownloadRequest`.
public func download(resumingWith resumeData: Data, to destination: Alamofire.DownloadRequest.DownloadFileDestination? = default) -> Alamofire.DownloadRequest

这个接口需要我们传入已存在的数据,然后基于我们传入的数据进行下载,它支持从新指定目的地路径,如果你有需要可以重新指定

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中便开始执行了任务

let op : BlockOperation = BlockOperation { [weak self] in
            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
                return (URL(fileURLWithPath: String(describing : NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, false)[0]+"123.mp4")), [.createIntermediateDirectories, .removePreviousFile])
                }.downloadProgress { [weak self] (pro) in
                    let percent = Float(pro.completedUnitCount) / Float(pro.totalUnitCount)
                    if count == 0 {

                        self?.downLoadLabel.snp.remakeConstraints { (make) in
                            make.width.equalTo(300 * percent)
                            make.height.equalTo(30)
                            make.top.equalTo((self?.stopButton.snp.bottom)!).offset(30)
                            make.left.equalToSuperview().offset(30)
                        }
                    } else {
                        self?.downLoadLabel2.snp.remakeConstraints { (make) in
                            make.width.equalTo(300 * percent)
                            make.height.equalTo(30)
                            make.top.equalTo((self?.downLoadLabel.snp.bottom)!).offset(30)
                            make.left.equalToSuperview().offset(30)
                        }
                    }
                }.responseJSON { (response) in

                    switch response.result {

                    case .success:
                        print("success")
                    case .failure:
                        self?.tmpData = response.resumeData

                    default:
                        print("failed")
                    }

            }
        }

        queue.addOperation(op)

每一个opeeation对象我们都可以设置他的优先级、启动、暂停、等属性,简单的调用接口就可以,在此就不一一作解释了。然后我们需要对我们的queue进行设置,我们设置最大并发数,大家可以根据实际情况来设置,demo中我只有两个下载任务,所以我就设置最大并发数为1 这样就是一个一个下载。

let queue : OperationQueue = {
        let que : OperationQueue = OperationQueue()
        que.maxConcurrentOperationCount = 1
        return que
    }()

我们运行然后点击开始下载

image

很奇怪我们发现他还是同时下载,我们又试了其他的个数,无论多少都是同时下载,最大线程数量完全不起作用,再反过来看下上面加入queue的任务。正常来说每一个operation都要等上一个operation完成后才会执行,而系统判断完成的标准就是上一个operation的闭包走完,我们闭包中放入的是一个下载任务,而Alamofire的下载都是异步执行,所以导致operation的闭包走完了,但是其实下载是异步在另一个线程执行的,实际上下载没有完成,知道原因我们对症下药,只需要保证operation闭包中的代码是同步执行的就OK了。而Alamofire是基于URLSession来实现的,并没有像connection那样提供同步的方法,所以我们使用信号量卡一下,像这样

image

这样之后就会按照我们设置好的队列进行了

[图片上传失败...(image-def8d0-1522483029727)]

有人会说下载同步进行会不会有影响,其实不会的首先我们实现同步的方式是信号量,本质上还是异步的只是我们阻塞的当前的下载线程,这个被阻塞线程一定不是主线程(除非Alamofire的开发者把他回调到主线程下载,这个基本不可能),而且当我们把这个下载任务加到一个operation中之后,就注定不会在主线程中了,没一个operation都会被系统分配到一个非主线程的地方去做,所以这样不会性能有任何影响。

总结

因为时间紧迫,暂时做了这么多,也遇到了这些问题,所以写出了总结下,本文还会继续更新,会慢慢的整个网络层分享出来。就是可能更新会慢,毕竟工作量有点饱和。多谢关注

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

推荐阅读更多精彩内容