谈谈如何使用Swift写出iOS断点续传下载大文件

本篇讲述实现iOS文件下载功能,包含大文件下载,后台下载,杀死进程,重新启动时继续下载,设置下载并发数,监听网络改变等。

预览效果

IMG_0686.gif

附上Demo地址,如果觉得还行呢,就麻烦顺手给个star

下载功能的实现

使用的网络连接的类为URLSession。在iOS7时推出,至此iOS系统才有了后台传输。在初始化URLSession前,需要先创建URLSessionConfiguration,可以理解为是URLSession需要的一个配置。URLSessionConfiguration有三种模式:

  • default:可以使用缓存的Cache、Cookie、鉴权。
  • ephemeral:仅内存缓存,不使用缓存的Cache、Cookie、鉴权。
  • background:支持后台传输,需要一个identifier标识,用来重新连接session对象。
let configuration = URLSessionConfiguration.background(withIdentifier: "CXDownloadBackgroundSessionIdentifier"

创建URLSession,设置配信息、代理、代理线程:

// Create `URLSession`, configure information, proxy, proxy thread.
session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)

在实现下载前,还需要了解一个很重要的类:URLSessionTask,无论下载多少文件,我们只需要初始化一个URLSession即可,而每个task对应一个任务,需要通过task才能实现下载,URLSessionTask是一个基类,有四个子类:

  • URLSessionDataTask:下载时,内容Data对象返回,需要我们不断写入文件。
  • URLSessionUploadTask:继承URLSessionDataTask,内容以Data对象返回,协议方法中可以查看请求时上传内容的过程,支持后台传输。
  • URLSessionStreamTask:建立了一个TCP/IP连接,替代InputStream/OutputStream,新的API可异步读写,自动通过HTTP代理连接远程服务器。
  • URLSessionDownloadTask:推荐使用该task实现文件下载,断点续传系统帮我们做了,资源会下载到一个临时文件,下载完成需将文件移动至想要的路径,系统会删除临时路劲文件,暂停时,系统会返回Data对象,恢复下载时用这个data创建task,支持后台传输。

后台下载

到这里,已经可以通过URLSessionDataTask实现断点续传了,下面介绍如何实现后台下载,其实非常简单,一共三步:

  1. 创建URLSession时,需要创建后台模式URLSessionConfiguration,上面已经介绍过了。
  2. 在AppDelegate中实现下面方法,并定义变量保存completionHandler代码块:
// 应用处于后台,所有下载任务完成调用
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    CXDownloadManager.shared.setDidFinishEventsForBackgroundURLSession(completionHandler: completionHandler)
}
  1. 在下载类中实现下面URLSessionDelegate协议方法,其实就是先执行完task的协议,保存数据、刷新界面之后再执行在AppDelegate中保存的代码块:
// 应用处于后台,所有下载任务完成及URLSession协议调用之后调
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    // Execute the block, the system generates a snapshot in the background, and releases the assertion that prevents the application from being suspended.
    didFinishEventsForBackgroundURLSessionHandler?()
}

程序终止,再次启动继续下载:

后台下载实现之后,再看一下如何实现进程杀死后,再次启动时继续下载,在应用程序被杀掉时,系统会自动保存应用下载session信息,重新启动应用时,如果创建和之前相同identifier的session,系统会找到对应的session数据,并响应urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)方法,操作如下:

 public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        guard let url = task.taskDescription else {
            return
        }
        // Process killed when downloading, callback error when restarting.
        if let err = error as? NSError,
           let reason = err.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] {
            CXDLogger.log(message: "Reason=\(reason)", level: .info)
            guard let model = CXDownloadDatabaseManager.shared.getModel(by: url)
            else {
                return
            }
            model.state = .waiting
            CXDownloadDatabaseManager.shared.updateModel(model, option: .state)
            return
        }
        let taskProcessor = downloadTaskDict[url]
        taskProcessor?.processSession(task: task, didCompleteWithError: error)
    }

并发数设置

下面介绍一下下载并发数的设置:URLSession本身就支持多任务同时下载,它会根据性能内部控制同时下载的个数,建议最多设置5个。一个任务对应一个URLSessionDataTask,所以想多任务同时下载,需要创建多个task,可以用数组或字典保存。我们定义变量去记录当前下载文件个数及用户设置的最大下载个数。

监听网络改变

用AFN监听,可以点击这里查看

为了增加用户体验,往往在设置中会给用户一个选项, 选择蜂窝网络下是否允许下载。URLSessionConfiguration本身就有一个属性allowsCellularAccess,默认为YES,允许蜂窝网络下载。如果不需要用户随时变更这个选项,是可以用这个属性。但是对于正在下载的任务,修改这个属性是无效的,即我们已经通过session创建了task对象,开启了任务,再试图用。

private func setup() {
        // Creates a database and a table.
        _ = CXDownloadDatabaseManager.shared
        
        currentCount = 0
        let ud = UserDefaults.standard
        let tmaxConcurrentCount = ud.integer(forKey: CXDownloadConfig.maxConcurrentCountKey)
        maxConcurrentCount = tmaxConcurrentCount > 0 ? tmaxConcurrentCount : 1
        allowsCellularAccess = ud.bool(forKey: CXDownloadConfig.allowsCellularAccessKey)
        
        lock = NSLock()
        
        // Single-threaded proxy queue.
        queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        
        // Defines the background session identifier.
        let configuration = URLSessionConfiguration.background(withIdentifier: "CXDownloadBackgroundSessionIdentifier")
        // Allows cellular network download, the default is true, which is turned on here. We added a variable to control the user's switching choice.
        configuration.allowsCellularAccess = true
        
        // Create `URLSession`, configure information, proxy, proxy thread.
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)
 }   

所以创建URLSessionConfiguration时把allowsCellularAccess设为YES,然后定义一个变量去控制是否允许蜂窝网络下载,在网络状态改变及用户设置修改这个选项之后,调用暂停、开启任务。

数据保存

用FMDB存储数据,可以点击这里查看

下载速度计算

声明两个变量,一个记录时间,一个记录在特定时间内接收到的数据大小,在接收服务器返回数据的urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)方法中,统计接收到数据的大小,达到时间限定时,计算速度=数据/时间,然后清空变量,为方便数据库存储,这里用的时间戳。

func processSession(dataTask: URLSessionDataTask, didReceive data: Data) {
        let receivedBytes = dataTask.countOfBytesReceived + resumedFileSize
        let allBytes = dataTask.countOfBytesExpectedToReceive + resumedFileSize
        model.totalFileSize = allBytes
        model.tmpFileSize = receivedBytes
        
        let dataLength = data.count
        // Calculates the size of the downloaded file within the speed time.
        model.intervalFileSize += Int64(dataLength)
        
        let intervals = CXDToolbox.getIntervalsWithTimestamp(model.lastSpeedTime)
        if intervals > 1 {
            // Calculates speed
            model.speed = model.intervalFileSize / intervals
            
            model.lastSpeedTime = CXDToolbox.getTimestampWithDate(Date())
        }
        
        let progress = Float(receivedBytes) / Float(allBytes)
        //CXDLogger.log(message: "progress: \(progress)", level: .info)
        model.progress = progress
        
        // Update the specified model in database.
        CXDownloadDatabaseManager.shared.updateModel(model, option: .progressData)
        postProgressNotification()
        // Reset it.
        model.intervalFileSize = 0
}

点赞+关注,第一时间获取技术干货和最新知识点,谢谢你的支持!

最后祝大家生活愉快~

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

推荐阅读更多精彩内容