在处理大文件的时候,我们不可能只是单一的去下载,那么我们就需要用到断点下载,当然你可以使用第三方实现断点下载,但是我们有时也要知道系统自带的怎么用,万一你使用的第三方不维护了,就麻烦了,当然这种概率很小。今天小编主要介绍NSURLSession
实现断点下载,NSURLConnection
也可以实现,NSURLConnection
在iOS9被苹果废弃了,所以还是使用NSURLSession
吧,下面我们先看下效果。
断点下载思路
先介绍一下单任务下载,实现方式
- NSMutableData拼接
使用会消耗大量内存,不用这个 - NSURLConnection
iOS9被废弃了也不用 - NSURLSessionDataTask
使用方式和NSURLConnection
差不多,需要我们自己实现下载路径 - NSURLSessionDownloadTask
使用很方便,默认下载到tmp文件
我所知道的就这四种,欢迎大家补充,我是使用NSURLSessionDataTask
实现的。
创建Session,NSURLSessionConfiguration
是配置信息,使用默认的
func initSession() -> URLSession {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.liuchang.cn")
let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
return session;
}
获取NSURLSessionDataTask
需要手动开始
let dataTask = session.dataTaskWithRequest(request)
dataTask.resume()
下面是用到的代理方法
收到响应。需要注意的是需要把NSURLSessionResponseDisposition
设为Allow,
如果NSHTTPURLResponse
的expectedContentLength = -1
需要服务员设置大小.
// 收到响应
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
这个方法会多次调用,在这里把收到的数据存到本地,使用NSOutputStream
可以实现拼接数据到本地。这里需要注意一下,OC中默认使用了多线程,在swift中默认在主线程执行
// 获取data 会多次调用
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
下载成功或者失败会调用,在这里关闭NSOutputStream
// 下载完成
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
多任务断点下载思路
既然有多个任务要么数组要么字典,但是在这里使用数组没有字典容易控制数据,我们需要创建一个model,把model保存到字典
enum LCDownloadState {
case Running // 下载中
case Suspended // 暂停
case Canceled // 取消
case Completed // 下载完成
case Failed // 下载失败
}
class LCDownload: NSObject {
var dataTask: NSURLSessionDataTask?
var outputStream: NSOutputStream?
var allLength: Int = 0
var progressBlock: ((progress: CGFloat) -> Void)?
var stateBlock: ((state: LCDownloadState) -> Void)?
}
下面我们需要创建一个单例,这也是为了用户可以边下载边浏览其他内容
private static let sDownload = LCSwiftDownload()
class var sharedInstance: LCSwiftDownload {
return sDownload
}
添加下载任务,根据字典判断是否有下载任务,没有就创建session,这里请求的使用添加Range
是为了杀死程序时,下次再进来的时候根据本地已经下载的大小去请求数据,就不需要重新请求。这里使用KVC修改taskIdentifier
为了在代理中判断是哪个任务。
/**
在点击事件中使用
- parameter url: url
- parameter tag: 唯一标识
- parameter resume: 是否开始下载
- parameter progerss: 进度 可以为nil
- parameter state: 状态 可以为nil
*/
func downloadData(url: String, tag: Int, resume: Bool, progerss: ((Float) -> Void)?, state: ((LCDownloadState) -> Void)?) {
let fileLength = getFileDataDownloadedLength(tag: tag)
let allLength = getAllLength(tag: tag)
if fileLength > 0 && fileLength == allLength {
state?(.Completed)
progerss?(1.0)
return
}
let tagStr = String(tag)
if let download = downloadDic[tagStr] {
let dataTask = download.dataTask
if resume {
dataTask?.resume()
download.stateBlock?(.Running)
}else {
dataTask?.suspend()
download.stateBlock?(.Suspended)
}
}else {
var request = URLRequest(url: URL(string: url)!)
let session = initSession()
request.setValue("bytes=\(fileLength)-", forHTTPHeaderField: "Range")
let dataTask = session.dataTask(with: request)
dataTask.setValue(tag, forKey: "taskIdentifier")
let download = LCDownload()
download.dataTask = dataTask
download.progressBlock = progerss
download.stateBlock = state
downloadDic[tagStr] = download
if resume {
dataTask.resume()
}
}
}
下面是代理方法:
收到响应时主要是获取总大小并保存到沙盒,使用expectedContentLength
能直接获取请求总大小,并且开启outputStream
// 收到响应
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let response = response as! HTTPURLResponse
let allLength = Int(response.expectedContentLength) + getFileDataDownloadedLength(tag: dataTask.taskIdentifier)
setAllLength(length: Int64(allLength), WithTag: dataTask.taskIdentifier)
if let download = downloadDic[String(dataTask.taskIdentifier)] {
let path = initFileDataCachePath(tag: dataTask.taskIdentifier)
print(path)
download.outputStream = OutputStream.init(toFileAtPath: path, append: true)
download.outputStream!.open()
download.allLength = Int(allLength)
}
completionHandler(.allow)
}
在这里把获取的数据保存到沙盒,并且根据本地下载数据获取下载比例,UnsafePointer
是swift中的指针,用的很少。
// 获取data 会多次调用
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if let download = downloadDic[String(dataTask.taskIdentifier)] {
let downloadedLength = getFileDataDownloadedLength(tag: dataTask.taskIdentifier)
let dataMutablePointer = UnsafeMutablePointer<UInt8>.allocate(capacity: data.count)
//Copies the bytes to the Mutable Pointer
data.copyBytes(to: dataMutablePointer, count: data.count)
//Cast to regular UnsafePointer
let dataPointer = UnsafePointer<UInt8>(dataMutablePointer)
//Your stream
download.outputStream?.write(dataPointer, maxLength: data.count)
let progress = Float(downloadedLength) / Float(download.allLength)
download.stateBlock?(.Running)
download.progressBlock?(progress)
}
}
下载完成关闭outputStream并且删除完成的model
// 下载完成
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let download = downloadDic[String(task.taskIdentifier)] {
download.stateBlock?(.Completed)
download.progressBlock?(1.0)
download.outputStream?.close()
download.outputStream = nil
downloadDic.removeValue(forKey: String(task.taskIdentifier))
if error != nil {
download.stateBlock?(.Failed)
}
}
}
以上是主要的思路,在这里下载代码代码中包括OC和Swift,有写地方需要优化的地方希望大家指出
最后更新内容:
- OC代码下载唯一标识改用URL
- 适配swift4.2