Swift--URLsession后台下载

前言

URLSession是一个可以响应发送或者接受HTTP请求的关键类。首先使用全局的 URLSession.shareddownloadTask 来创建一个简单的下载任务:

let url = URL(string: "https://mobileappsuat.pwchk.com/MobileAppsManage/UploadFiles/20190719144725271.png")
let request = URLRequest(url: url!)
let session = URLSession.shared
let downloadTask = session.downloadTask(with: request,
       completionHandler: { (location:URL?, response:URLResponse?, error:Error?)
        -> Void in
        print("location:\(location)")
        let locationPath = location!.path
        let documnets:String = NSHomeDirectory() + "/Documents/1.png"
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)
        print("new location:\(documnets)")
    })
downloadTask.resume()

可以看到这里的下载是前台下载,也就是说如果程序退到后台(比如按下 home 键、或者切到其它应用程序上),当前的下载任务便会立刻停止,这个样话对于一些较大的文件,下载过程中用户无法切换到后台,对用户来说是一种不太友好的体验。下面来看一下在后台下载的具体实现:

URLsession后台下载

我们可以通过URLSessionConfiguration类新建URLSession实例,而URLSessionConfiguration这个类是有三种模式的:

URLSessionConfiguration 的三种模如下式:

  • default:默认会话模式(使用的是基于磁盘缓存的持久化策略,通常使用最多的也是这种模式,在default模式下系统会创建一个持久化的缓存并在用户的钥匙串中存储证书)
  • ephemeral:暂时会话模式(该模式不使用磁盘保存任何数据。而是保存在 RAM 中,所有内容的生命周期都与session相同,因此当session会话无效时,这些缓存的数据就会被自动清空。)
  • background:后台会话模式(该模式可以在后台完成上传和下载。)

注意:background模式与default模式非常相似,不过background模式会用一个独立线程来进行数据传输。background模式可以在程序挂起,退出,崩溃的情况下运行task。也可以在APP下次启动的时候,利用标识符来恢复下载。

下面先来创建一个后台下载的任务background session,并且指定一个 identifier

let urlstring = URL(string: "https://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg")!

// 第一步:初始化一个background后台模式的会话配置configuration
let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.cn")
 
// 第二步:根据配置的configuration,初始化一个session会话
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

// 第三步:传入URL,创建downloadTask下载任务,开始下载
session.downloadTask(with: url).resume()

接下来实现session的下载代理URLSessionDownloadDelegateURLSessionDelegate的方法:

extension ViewController:URLSessionDownloadDelegate{
    // 下载代理方法,下载结束
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下载完成 - 开始沙盒迁移
        print("下载完成地址 - \(location)")
        let locationPath = location.path
        //拷贝到用户目录
        let documnets = NSHomeDirectory() + "/Documents/" + "com.Henry.cn" + ".dmg"
        print("移动到新地址:\(documnets)")
        //创建文件管理器
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)

    }
    //下载代理方法,监听下载进度
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        print(" bytesWritten \(bytesWritten)\n totalBytesWritten \(totalBytesWritten)\n totalBytesExpectedToWrite \(totalBytesExpectedToWrite)")
        print("下载进度: \(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))\n")
    }
}

设置完这些代码之后,还不能达到后台下载的目的,还需要在AppDelegate中开启后台下载的权限,实现handleEventsForBackgroundURLSession方法:

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    //用于保存后台下载的completionHandler
    var backgroundSessionCompletionHandler: (() -> Void)?
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        self.backgroundSessionCompletionHandler = completionHandler
    }
}

实现到这里已基本实现了后台下载的功能,在应用程序切换到后台之后,session 会和 ApplicationDelegate 做交互,session 中的task还会继续下载,当所有的task完成之后(无论下载失败还是成功),系统都会调用ApplicationDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回调,在处理事件之后,在 completionHandler参数中执行 闭包,这样应用程序就可以获取用户界面的刷新。

如果我们查看handleEventsForBackgroundURLSession这个api的话,会发现苹果文档要求在实现下载完成后需要实现URLSessionDidFinishEvents的代理,以达到更新屏幕的目的。

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("后台任务")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
        backgroundHandle()
    }
}

如果没有实现此方法的话⚠️️:后台下载的实现是不会有影响的,只是在应用程序由后台切换到前台的过程中可能会造成卡顿或者掉帧,同时可能在控制台输出警告:

Alamofire后台下载

通过上面的例子🌰会发现如果要实现一个后台下载,需要写很多的代码,同时还要注意后台下载权限的开启,完成下载之后回调的实现,漏掉了任何一步,后台下载都不可能完美的实现,下面就来对比一下,在Alamofire中是怎么实现后台下载的。

首先先创建一个ZHBackgroundManger的后台下载管理类:

struct ZHBackgroundManger {    
    static let shared = ZHBackgroundManger()

    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.AlamofireDemo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = 10
        configuration.timeoutIntervalForResource = 10
        configuration.sharedContainerIdentifier = "com.Henry.AlamofireDemo"
        return SessionManager(configuration: configuration)
    }()
}

后台下载的实现:

ZHBackgroundManger.shared.manager
    .download(self.urlDownloadStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
    let documentUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
    let fileUrl     = documentUrl?.appendingPathComponent(response.suggestedFilename!)
    return (fileUrl!,[.removePreviousFile,.createIntermediateDirectories])
    }
    .response { (downloadResponse) in
        print("下载回调信息: \(downloadResponse)")
    }
    .downloadProgress { (progress) in
        print("下载进度 : \(progress)")
}

并在AppDelegate做统一的处理:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    ZHBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}

这里可能会有疑问🤔,为甚么要创建一个ZHBackgroundManger单例类?

那么下面就带着这个疑问❓来探究一下

如果点击ZHBackgroundManger.shared.manager.download这里的manager会发现这是SessionManager,那么就跟进去SessionManager的源码来看一下:


可以看到在SessionManagerdefault方法中,是对URLSessionConfiguration做了一些配置,并初始化SessionManager.

那么再来看SessionManager的初始化方法:


SessionManagerinit初始化方法中,可以看到这里把URLSessionConfiguration设置成default模式,在内容的前篇,在创建一个URLSession的后台下载的时候,我们已经知道需要把URLSessionConfiguration设置成background模式才可以。

在初始化方法里还有一个SessionDelegatedelegate,而且这个delegate被传入到URLSession中作为其代理,并且session的这个初始化也就使得以后的回调都将会由 self.delegate 来处理了。也就是SessionManager实例创建一个SessionDelegate对象来处理底层URLSession生成的不同类型的代理回调。(这又称为代理移交)。

代理移交之后,在commonInit()的方法中会做另外的一些配置信息:


在这里delegate.sessionManager被设置为自身 self,而 self其实是持有 delegate 的。而且 delegatesessionManagerweak属性修饰符。

这里这么写delegate.sessionManager = self的原因是

  • delegate在处理回调的时候可以和sessionManager进行通信
  • delegate将不属于自己的回调处理重新交给sessionManager进行再次分发
  • 减少与其他逻辑内容的依赖

而且这里的delegate.sessionDidFinishEventsForBackgroundURLSession闭包,只要后台任务下载完成就会回调到这个闭包内部,在闭包内部,回调了主线程,调用了 backgroundCompletionHandler,这也就是在AppDelegateapplication:handleEventsForBackgroundURLSession方法中的completionHandler。至此,SessionManager的流程大概就是这样。

对于上面的疑问:

  • 1. 通过源码我们可以知道SessionManager在设置URLSessionConfiguration的默认的是default模式,因为需要后台下载的话,就需要把URLSessionConfiguration的模式修改为background模式。包括我们也可以修改URLSessionConfiguration其他的配置
  • 2. 在下载的时候,应用程序进入到后台下载,如果对于上面的配置,不做成一个单例的话,或者没有被持有的情况下,在进入后台后就会被释放掉,从而会产生错误Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 3. 而且将SessionManager重新包装成一个单例后,在AppDelegate中的代理方法中可以直接使用。

总结

  • 首先在 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里,把回调闭包completionHandler传给了 SessionManagerbackgroundCompletionHandler.
  • 在下载完成的时候 SessionDelegateurlSessionDidFinishEvents代理的调用会触发 SessionManagersessionDidFinishEventsForBackgroundURLSession代理的调用
  • 然后sessionDidFinishEventsForBackgroundURLSession 执行SessionManagerbackgroundCompletionHandler的闭包.
  • 最后会来到 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里的 completionHandler 的调用.

关于Alamofire后台下载的代码就分析到这里,其实通过源码发现,和利用URLSession进行后台下载原理是大致相同的,只不过利用Alamofire使代码看起来更加简介,而且Alamofire中会有很多默认的配置,我们只需要修改需要的配置项即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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