【iOS开发】URLSession简介 & 大文件下载断点续传

首先介绍下这个Demo:点击开始下载后,开始下载一张图片;可以暂停,然后继续下载;上面可以显示下载进度;下载完成后,把下面的图片替换成我们下载的图片。

界面

为了实现下载功能,这里用到的一个主要类URLSession,下面首先介绍一下这个类。

URLSession

URLSession类原生支持datafileftphttphttps协议。跟它比较相关的另外一个类是URLSessionConfiguration,这个类可以用来设置会话的相关配置,在初始化会话实例时,有可能会用到。

URLSession类的层次结构

URLSession类的层次结构如下:

  • URLSession:一个会话对象
  • URLSessionConfiguration:一个会话配置对象,用户初始化会话
  • URLSessionTask:一个会话任务基类,下面三个是它的子类
    • URLSessionDataTask:获取URL内容并把获取到的内容作为Data的任务
      • URLSessionUploadTaskURLSessionDataTask的子类,一个上传文件的任务
    • URLSessionDownloadTask:获取URL内容并把获取到的内容存储到ROM的任务
    • URLSessionStreamTask:用于建立TCP/IP连接的任务
URLSessionAPI还提供了以下代理协议
  • URLSessionDelegate:定义了处理会话级别的代理方法
  • URLSessionTaskDelegate:定义了处理任务级别的代理方法
  • URLSessionDataDelegate:定义了处理数据和上传任务级别的代理方法
  • URLSessionStreamDelegate:定义了处理流任务级别的代理方法
初始化URLSession实例

在初始化一个URLSession的实例时,可以有四种情况,每一种情况处理不同的请求:

  • URLSession有一个单例shared,这个单例没有设置会话的相关配置,可以用于基本请求。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration.default。通过这种方式初始化的实例,类似于单例,但是这种方式可以设置一个代理,然后获取相关数据。这种方式会把缓存保存到ROM中、把证书保存到用户的钥匙链,同时也会保存cookie。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration. ephemeral,通过这种方式初始化的实例,类似于第二种创建方式,但是这种方式不会把缓存、证书和其他与会话相关的数据保存到ROM中,而是保存在RAM中,当会话失效后,这些数据会被自动清除。除非你自己手动保存到文件中。这种模式的优点主要是能保护用户的隐私。
  • 使用URLSession的初始化器,传入会话配置对象URLSessionConfiguration.background(withIdentifier: "download"),通过这种方式初始化的实例,可以支持HTTPHTTPS在后台上传和下载文件。如果应用被系统杀死或者重新启动,应用可以使用同一个identifier来创建一个新的会话配置对象,并获取被系统杀死时的传输状态。如果是被用户手动终止程序,那么系统会取消所有这个会话控制的后台传输,而且系统不会自动重新打开程序。只有用户手动重新启动应用,然后继续下载任务。让应用支持后台上传和下载,还需要把会话配置对象的属性isDiscretionary设置为true
URLSession的使用步骤
  • 根据实际情况,创建一个会话配置URLSessionConfiguration对象
  • 创建会话URLSession对象
  • 使用会话对象创建一个任务
  • 调用任务的resume()方法,开始执行任务

任务开始之后,会话将会调用以下代理方法:

  1. 最初与服务器握手需要一个连接认证,例如SSL客户端证书,会话调用urlSession(_:task:didReceive:completionHandler:)或者urlSession(_:didReceive:completionHandler:)代理方法。
  2. 如果任务数据使用一个流提供的,调用urlSession(_:task:needNewBodyStream:)方法来获取一个InputStream对象,以为新的请求提供数据。
  3. 在最初传数据给服务器时,代理将会周期性地收到urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)回调,在这个回调中,描述了上传过程。
  4. 服务器发回响应
  5. 如果响应里面要求需要认证,会话调用urlSession(_:task:didReceive:completionHandler:)。回到第二步。
  6. 如果响应是一个HTTP回调响应,会话调用urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)。在这个代理方法里面,使用已提供的URLRequest对象、一个新的URLRequest对象(用于回调一个不同的URL)或者nil(把回调的响应体最为一个有效的响应并且把它返回最为结果)来执行completionHandler
  • 如果使用了这个回调,回到第二步。
  • 如果代理没有实现这个方法,回调会跟踪回调的最大数量。
  1. 对于使用downloadTask(withResumeData:)或者downloadTask(withResumeData:completionHandler:)创建的下载任务,会调用urlSession(_:downloadTask:didResumeAtOffset:expectedTotalBytes:)
  2. 对于一个数据任务,会调用urlSession(_:dataTask:didReceive:completionHandler:)。决定是否把数据任务转换为下载任务,然后调用completionHandler来继续接收数据或者下载数据。
  • 如果选择把数据任务转换为下载任务,会话会调用urlSession(_:dataTask:didBecome:),调用这个方法之后,代理不会在从数据任务收到回调,而是收到下载任务回调。
  1. 在从服务器传输过程中,代理会周期性的收到任务级别的回调来报告传输的过程。
  • 对于数据任务,会话调用urlSession(_:dataTask:didReceive:)
  • 对于下载任务,会话调用urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)。使用任务的cancel(byProducingResumeData:)方法来取消下载。通过任务的downloadTask(withResumeData:)downloadTask(withResumeData:completionHandler:)来开启一个新的下载任务继续下载,回到第一步。
  1. 对于一个数据任务,会话可能会调用urlSession(_:dataTask:willCacheResponse:completionHandler:)。在这里,你要决定是否允许缓存。如果没有实现这个方法,那么将会默认使用会话配置对象指定的缓存策略来执行。
  2. 如果响应是多重编码的,会话可能会调用多次didReceiveResponse方法。
  3. 如果一个下载任务完成,会话调用urlSession(_:downloadTask:didFinishDownloadingTo:),这个方法中有一个文件临时存放的位置。我们需要在这里使用这些数据或者把数据保存到一个永久的位置。
  4. 当任务完成时,会话调用urlSession(_:task:didCompleteWithError:),这个方法中有一个可选的error,如果errornil,那么意味着文件已经下载完成。如果一个下载任务被用户暂停,下载任务可以继续,并且error不为nil,这个error中包含了一个userInfo字典,NSURLSessionDownloadTaskResumeData键对应的值就是目前已经下载的数据,我要要保存起来供继续下载使用。调用会话的downloadTask(withResumeData:)或者downloadTask(withResumeData:completionHandler:)方法新建一个下载任务,继续下载未完成的内容。如果这个任务不能继续,那么需要重新新建一个下载任务,重新下载。

代码演示

用一个结构来存储这个控制器要用到的常量:

private struct Constants {
    static let kDownload = "Download"
    static let kStartDownload = "开始下载"
    static let kPauseDownload = "暂停下载"
    static let kResumeDownload = "继续下载"
    static let kCompleteDownload = "下载完成"
}

点击下载按钮后,根据按钮的标题做相应的操作:

    @IBAction func startDownload(_ button: UIButton) {
        switch button.currentTitle! {
            
        case Constants.kStartDownload:
            currentTitle = Constants.kPauseDownload
            UIApplication.shared.isNetworkActivityIndicatorVisible = true
            
            // 创建会话相关配置
            let config = URLSessionConfiguration.background(withIdentifier: Constants.kDownload)
            // 在应用进入后台时,让系统决定决定是否在后台继续下载。如果是false,进入后台将暂停下载
            config.isDiscretionary = true
            
            // 创建一个可以在后台下载的session (其实会话的类型有四种形式)
            session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
            task = session.downloadTask(with: request)
            task.resume()
            
        case Constants.kPauseDownload:
            UIApplication.shared.isNetworkActivityIndicatorVisible = false
            currentTitle = Constants.kResumeDownload
            
            // 保存已经下载的位置
            task.cancel { (data) in
                self.resumeData = data
            }
            
        case Constants.kResumeDownload:
            UIApplication.shared.isNetworkActivityIndicatorVisible = true
            currentTitle = Constants.kPauseDownload
            
            // 重新建立一个下载任务,继续下载未完成的数据
            task = session.downloadTask(withResumeData: resumeData)
            task.resume()
            
        case Constants.kCompleteDownload:
            print("kCompleteDownload-----下载完成")
            
        default:
            break
        }
    }

下面是实现URLSessionDownloadDelegate的代理方法:

extension ViewController: URLSessionDownloadDelegate {
    
    // 每下载完一部分调用,可能会调用多次
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        progressView.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        
        print(progressView.progress)
    }
    
    // 下载完成后调用
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下载完成后,保存到缓存目录
        let destination = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).last! + "/" + downloadTask.response!.suggestedFilename!
        do {
            try FileManager.default.moveItem(atPath: location.path, toPath: destination)
        }
        catch {
            print(error.localizedDescription)
        }
        
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        imageView.image = UIImage(contentsOfFile: destination)
        currentTitle = Constants.kCompleteDownload
        // 在后台下载完成,重新进入前台,把progress设置为1.0;如果不设置,progress的值是进入后台之前的值
        progressView.progress = 1.0
        session.invalidateAndCancel() // 下载完成,使session失效
    }
    
    // 任务完成时调用,但是不一定下载完成;用户点击暂停后,也会调用这个方法
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if error != nil {
            // 如果下载任务可以恢复,那么NSError的userInfo包含了NSURLSessionDownloadTaskResumeData键对应的数据,保存起来,继续下载要用到
            if let data = (error as! NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                resumeData =  data
            }
        }
    }
    
    // 继续下载时调用
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
        print("didResumeAtOffset-----继续下载")
    }
}

关键代码都在这里,大家可以下载Demo。有什么问题,欢迎留言。谢谢!

Demo地址 >>

如果文中有错误,请指出!我们共同学习,共同进步。谢谢!

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

推荐阅读更多精彩内容