URLSession 下载数据

URLSession是一个完整的网络API,用于通过HTTP上传和下载内容。

URL loading system包括加载URL的类以及许多重要的帮助程序类,这些辅助程序类与这些URL loading 一起使用以修改其行为。主要帮助程序类分为五类:协议支持,身份验证和凭据,cookie存储,配置管理和缓存管理。
简单来说URL loading system 就是一组用于与服务器通信的类.下图有点老了是2013年的版本, 最新的版本没找到.


URL loading system

使用URLSessionAPI,您的应用会创建一个或多个会话,每个会话都会协调一组相关的数据传输任务。例如,你可以创建多个会话, 一个可以用于下载数据, 一个用于请求数据, 在每个会话中,您的应用程序会添加一系列任务,每个任务都代表对特定URL的请求。

URL会话的类型

URLSession是负责发送和接收HTTP请求的关键对象, URLSession具有shared基本请求的单例会话(没有配置对象)。创建的会话不可自定义. 对于其他类型的会话。通过URLSessionConfiguration创建,有三种形式:

.default:默认会话的行为与shared会话非常相似(除非您进一步自定义它们),但是他可以让您使用委托增量方式获取数据。您可以通过调用URLSessionConfiguration类上的默认方法来创建默认会话配置。
shared 是无法设置委托的, 所以只能使用系统提供的闭包处理数据

open func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

但是default会话是可以自定义委托对象, 使用委托回调来处理数据, 这里一定要注意以增量的方式获取数据, 这点与闭包区别很大, 下面会具体讲到

.ephemeral:临时会话与默认会话类似,但它们不会将高速缓存,cookie或凭据写入磁盘。您可以通过调用URLSessionConfiguration类上的临时方法来创建临时会话配置。
.background:允许会话在后台执行上载或下载任务。即使应用程序本身被系统暂停或终止,传输仍会继续。(这个很牛呀)

URLSessionConfiguration是一个配置会话的类, 例如, 请求的超时时长, 缓存策略, 请求头等等

URLSessionTask是一个表示任务对象的抽象类, 抽象类是不能直接创建对象来使用的, 须有其子类来完成, 子类会话创建一个或多个任务来执行获取数据和下载或上载文件的事件。

有三种类型的具体会话任务:

URLSessionDataTask:将此任务用于HTTP GET请求,以将数据从服务器检索到内存。
URLSessionUploadTask:使用此任务通常通过 POST或PUT方法将文件从磁盘上传到Web服务。
URLSessionDownloadTask:使用此任务将文件从远程服务下载到临时文件位置。

具体的工作流程首先创建一个会话URLSession, 然后使用URLSessionConfiguration来配置会话, 然后使用URLSession去调用task方法, 调用不同的task会返回对应的task任务, 然后由task执行resume()具体的任务.

每一个App里面不可能只存在一个网络请求, 存在多个网络请求, 如果这些网络请求的配置(URLSessionConfiguration)是相同的, 你不应该为每个请求创建一个URLSession, 而是创建一个URLSession单例对象, 在他们之间共享.


上面说到URLSessionTask有三种任务, 数据, 上传和下载任务

数据任务

获取数据有两种方式一种使用闭包(Receive Results with a Completion Handler), 一种使用委托(Receive Transfer Details and Results with a Delegate)

1. 使用闭包

获取数据的最简单方法是创建使用闭包处理程序的数据任务。通过这种方式, 不需要配置会话, 直接调用共享会话URLSession.shared,任务将服务器的响应,数据和可能的错误传递给您提供的闭包。最关键的是数据不是增量返回的, 是一次完整的返回, 不需要自己去处理增量数据.

创建完成处理程序以接收任务的结果
        let session = URLSession.shared
        let sessionTask = session.dataTask(with: request) { (data, response, error) in
            if let error = error {
                print(error as Any)
                return
            }
            guard let httpResponse = response as? HTTPURLResponse,
                (200...299).contains(httpResponse.statusCode) else {
                    print(response as Any)
                    return
            }
            if let mimeType = httpResponse.mimeType, mimeType == "application/json",
                let data = data {
                DispatchQueue.main.async {
                    let json = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                    print(json as Any)
                }
                
            }
        }
        sessionTask.resume()
  1. 验证error参数是否nil。如果不是,则发生传输错误; 处理错误并退出。
  2. 检查response参数以验证状态代码是否指示成功,以及MIME类型是否为预期值。如果没有,请处理服务器错误并退出。
    mimeType代表服务器给我们返回的数据类型, 根据这个字段来区分如何去解析数据, 如application/json表示JSON数据, text/html表示HTML数据等等
  3. data我们需要的数据, dataTask是异步任务, 一半获取数据之后如果要进行在主线程刷新UI, 这时需要回到主线程.
  4. 创建的task任务, 是处于挂起状态的, 他自己不会自动执行请求数据的任务, 需要手动执行resume()
    使用闭包处理有两种方法, 如下所示
open func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

open func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
2. 使用代理

共享URLSession虽然使用起来简单方便, 但是存在很多局限, 如果想使用更加完善的请求, 则可以使用代理.

实现委托以接收任务的结果

通过代理方法,数据是分批依次返回的,直到传输完成或失败出现错误。随着数据传输的进行,URLSessionDataDelegate会收到代理事件urlSession(_:dataTask:didReceive:)

在使用闭包处理时, 使用和系统给我们提供的单例对象shared, 我们自己创建时, 也应该使用单例对象, 因为URLSession在请求数据没有完成之前不能被释放掉, 否则, 数据将不能被请求
如下所示

 override func viewDidLoad() {
        super.viewDidLoad()
        let sessionConfigure = URLSessionConfiguration.default
        sessionConfigure.networkServiceType = .default
        let session = URLSession.init(configuration: sessionConfigure, delegate: self, delegateQueue: OperationQueue.main)
        let sessionTask = session.dataTask(with: request)
    }

这样URLSession会在viewDidLoad() 方法执行完毕之后就会被释放掉, 您将不会受到代理的回调数据. (这是一个巨坑, 这个和AVPlayer一样, AVPlayer如果被释放了, 音视频也将不能播放, 使用时要注意)

创建使用委托的URLSession

var receiveData:Data?

private lazy var session: URLSession = {
    let configuration = URLSessionConfiguration.default
    configuration.waitsForConnectivity = true
    return URLSession(configuration: configuration,
                      delegate: self, delegateQueue: nil)
}()

func delegateHandler(request:URLRequest) {
        let sessionTask = session.dataTask(with: request)
        receiveData = Data()
        sessionTask.resume()
}

receiveData用来拼接每次请求获取的数据, 懒加载session, 如果多次创建只会存在一个. delegateHandler 用来发起请求

URLSessionDataDelegate代理方法

  // 1
   func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        guard let response = response as? HTTPURLResponse,
            (200...299).contains(response.statusCode),
            let mimeType = response.mimeType,
            mimeType == "application/json" else {
                completionHandler(.cancel)
                return
        }
        completionHandler(URLSession.ResponseDisposition.allow)
    }
    //2
   func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        print("receive = \(data.count)")
        receiveData?.append(data)
    }
    //3
   func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if error != nil {
            print("fail")
        }else {
            if let data = receiveData {
                let json = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                print(json as Any)
            }
            print("success")
        }
    }
  1. 收到响应的回调, 在该代理方法中可以进行一些数据验证, 决定请求时继续还是取消, 或者其他的操作
  2. 依次获取的数据
  3. 请求结束或者出现错误的回调

下载任务

URLSessionDataTaskURLSessionDownloadTask都可以用来实现下载任务, 在上面使用代理的方式请求数据的时候, 代理方法
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
以增量的方式来获取数据, 我们下载文件的时候也可以使用这种方法.

以下载mp3文件为例

1. URLSessionDataTask下载方式
let url = URL.init(string: "")
let request = URLRequest.init(url: url!)
let sessionTask = session.dataTask(with: request)
receiveData = Data()
sessionTask.resume()

前面实现URLSessionDataDelegate的代理方法, 是用来接收JSON数据的, 既然我们要下载mp3文件, 就要对代理修改, 适应mp3 文件的下载
首先要创建一个下载路径

    var downloadPath = ""

      if let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first {
            print(documentsPath)
            downloadPath = documentsPath + "/download"
            do {
                try FileManager.default.createDirectory(atPath: downloadPath, withIntermediateDirectories: true, attributes: nil)
            } catch {
                print(error)
            }
        }
// URLSessionDataDelegate 代理方法
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        guard let response = response as? HTTPURLResponse,
            (200...299).contains(response.statusCode),
            let mimeType = response.mimeType,
            mimeType == "audio/mpeg" else { // 1. 
                completionHandler(.cancel)
                return
        }
        completionHandler(URLSession.ResponseDisposition.allow)
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        print("receive = \(data.count)")
        receiveData?.append(data)
        print("当前下载 = \(data.count), 已经下载 = \(dataTask.countOfBytesReceived), 总共需要下载 = \(dataTask.countOfBytesExpectedToReceive)")

    }
    
    // URLSessionDownloadDelegate 下载完成也会走这个方法,后走
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if error != nil {
            print("fail")
        }else {
            if let data = receiveData { // 2. 
                do {
                    // 如果名称相同, 会覆盖之前的数据
                    let url = URL.init(fileURLWithPath: downloadPath + "/\(Date().description).mp3")
                    try data.write(to: url, options: Data.WritingOptions.atomicWrite)
                } catch let error as NSError {
                    print(error.localizedDescription)
                }
            }
            print("success")
        }
    }
  1. mimeType的类型要修改成audio/mpeg
  2. 当数据下载完成时, 要把数据写入磁盘
  3. 需要注意的是, 当写入文件是, 如果名字相同, 后者会覆盖前者的文件
    并不是所有的文件都支持下载的, 这个要具体需要服务器来配置, 打印URLResponse, 会看到下面的内容
<NSHTTPURLResponse: 0x60000243ba20> { URL: http://.mp3 } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Content-Length" =     (
        6502067
    );
    "Content-Type" =     (
        "audio/mpeg"
    );
    Date =     (
        "Wed, 26 Dec 2018 14:59:59 GMT"
    );
    Etag =     (
        "\"8078c31b864ce1:0\""
    );
    "Last-Modified" =     (
        "Sun, 09 Jun 2013 02:22:29 GMT"
    );
    Server =     (
        "Microsoft-IIS/7.5"
    );
    "X-Powered-By" =     (
        "ASP.NET"
    );
} }
  1. Accept-Ranges 代表是否支持断点下载 bytes支持, none不支持
  2. Content-Length文件大小,以字节为单位的十进制数, 对应的expectedContentLength
  3. Content-Type文件类型, 对应的mimeType

一般的文件下载如果有一个下载进度条会更好, 但是URLSessionDataDelegate没有提供给我们进度更新的代理方法, 这个只能有我们自己去处理, 要计算进度需要三个值, 当前下载的字节数, 已经下载的字节数, 期望下载的字节数,

  1. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)的代理方法返回了每次请求到的数据
  2. 在响应头里既然已经返回了文件的大小, 可以通过expectedContentLength来获取期望下载的数据大小, 也可以通过URLSessionTask的countOfBytesExpectedToReceive获取
  3. 已经下载的字节数, 可使用累加每次下载的字节数来获取, 也可以通过URLSessionTask 的countOfBytesReceived获取
    如此, 可完成一个进度条的功能
2. URLSessionDownloadTask下载方式

虽然使用URLSessionDataTask也完成了下载, 但是需要我们自己去处理的事情有点多, 而URLSessionDownloadTask则是Apple封装的用于下载的任务, 包括断点下载都做了很好的封装处理, 下面使用URLSessionDownloadTask 来完成同样的mp3文件的下载

创建一个URLSessionDownloadTask任务

        let sessionTask = session.downloadTask(with: request)

实现URLSessionDownloadDelegate

   // 1
   func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print(location)
        do {
            try FileManager.default.moveItem(at: location, to: URL.init(fileURLWithPath: downloadPath + "/\(Date().description).mp3"))
        } catch let error as NSError {
            print(error)
        }
    }
    //2
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        print("当前下载 = \(bytesWritten), 已经下载 = \(totalBytesWritten), 总共需要下载 = \(totalBytesExpectedToWrite)")
        print("\(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) * 100)%")
        print(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
        DispatchQueue.main.async {
            self.progress.setProgress(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite), animated: true)
        }
    }
    //3
    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("fileOffset = \(fileOffset), expectedTotalBytes = \(expectedTotalBytes)")
    }

URLSessionDownloadDelegate有三个代理方法

  1. 下载完成的回调, 改回调的location是下载的文件路径, URLSessionDownloadTask任务会把文件下载到tmp文件, tmp文件是一个临时文件里面的数据会被系统删除, 所以当收到下载完成的回调时, 要立马把该文件移动到其他的文件目录下.
  2. 下载进度的回调, 下载任务是默认异步线程, 刷新UI需要回到UI线程
  3. 断点下载时的文件偏移量

需要注意的是, 如果实现了URLSessionDownloadDelegate, 则URLSessionDataDelegate的方法只有func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)会在文件下载完成好会执行, 在didFinishDownloadingTo代理执行完之后再执行该方法

每一个URLSessionDownloadTask管理者自己的下载任务, 创建完任务后, 每执行一次sessionTask.resume()便开始了一个异步下载任务, 不同的任务之间相互不影响. 如果要实现多个下载任务, 则只需自己处理好数据接受即可.

暂停下载, 继续下载, 取消下载

1. 暂停下载

如果暂停成功, 则在闭包里返回当前已经下载的文件, 如果继续下载只需把该文件存储起来, 以供继续下载时使用

open func cancel(byProducingResumeData completionHandler: @escaping (Data?) -> Void)
2. 继续下载
let sessionTask = session.downloadTask(withResumeData: data)
sessionTask.resume()

withResumeData参数是继续下载的文件, 传入该文件, 然后执行resume()方法便可以继续下载. 继续下载时会执行

public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("fileOffset = \(fileOffset), expectedTotalBytes = \(expectedTotalBytes)")
    }

fileOffset为已经下载的数据量, expectedTotalBytes为需要下载的数据量

这里有一个坑, 当我们使用open func downloadTask(with request: URLRequest) -> URLSessionDownloadTask创建一个下载任务时, 返回值是一个URLSessionDownloadTask, 是一个新的下载任务
当我们暂停下载, 在已经下载的基础上继续下载时open func downloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask, 该方法也是创建了一个新的下载任务, 返回值也是URLSessionDownloadTask, 与之前的未下载完成的那个下载任务已经没有关系了, 要恢复下载执行resume()方法时, 要使用downloadTask(withResumeData resumeData: Data)返回的下载任务. 否则无效.

只有满足以下条件,才能恢复下载:

  1. 自您第一次请求资源以来,资源没有变化
  2. 该任务是HTTP或HTTPS GET请求
  3. 服务器在其响应中提供ETag或Last-Modified标题(或两者)
  4. 服务器支持字节范围请求
  5. 系统尚未删除临时文件(如果磁盘不足, 系统会自动删除tmp里的文件)
3. 取消下载
 URLSessionTask.cancel()

该方法是不能恢复下载的

查看URLSessionTask头文件, 有一个方法suspend(), 该方法也是暂停操作, 暂停之后再执行resume()也是可以恢复下载. 但是系统没有方法给我们返回已经下载的内容, 以供以后继续下载. 如果App被杀掉, 则无法继续执行resume()来完成下载. 虽然suspend()方法会把已经下载的文件放在tmp文件夹里, 心想可以读取tmp里面的文件, 然后执行downloadTask(withResumeData: data)继续下载, 但是该文件的名称是没有规律的, 无法知道该文件的名称, 也就无法获取数据. (ps, 如果有更好的方法可以实现, 烦请告知下)

后台下载

let configuration = URLSessionConfiguration.background(withIdentifier: 
  "bgSessionConfiguration")

注意:后台配置只能创建一个会话,因为系统使用配置的标识符将任务与会话相关联.

使用后台配置模式, 当我们把应用程序退到后台时, 应用程序将继续在后台下载, 当下载完成时, 应用程序将会收到handleEventsForBackgroundURLSessionurlSessionDidFinishEvents的通知

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    }

 func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
        //更细UI
        }
    }

先执行AppDelegate的代理, 再执行URLSessionDelegate的代理, 可以在回调里面对下载文件做一些处理, 比如更新UI

开启后台要在后有一个疑问, 开启下载, tmp文件夹里没有临时文件, 如果不使用后台下载就会有临时文件, 不知道这个是什么原因.

https://cloud.tencent.com/developer/section/1189854

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

推荐阅读更多精彩内容