NSURLSession实现多任务断点下载
在处理大文件的时候,我们不可能只是单一的去下载,那么我们就需要用到断点下载,当然你可以使用第三方实现断点下载,但是我们有时也要知道系统自带的怎么用,万一你使用的第三方不维护了,就麻烦了,当然这种概率很小。今天小编主要介绍NSURLSession
实现断点下载,NSURLConnection
也可以实现,NSURLConnection
在iOS9被苹果废弃了,所以还是使用NSURLSession
吧,下面我们先看下效果。
断点下载思路
先介绍一下单任务下载,实现方式
- NSMutableData拼接
使用会消耗大量内存,不用这个 - NSURLConnection
iOS9被废弃了也不用 - NSURLSessionDataTask
使用方式和NSURLConnection
差不多,需要我们自己实现下载路径 - NSURLSessionDownloadTask
使用很方便,默认下载到tmp文件
我所知道的就这四种,欢迎大家补充,我是使用NSURLSessionDataTask
实现的。
创建Session,NSURLSessionConfiguration
是配置信息,使用默认的
func initSession() -> NSURLSession {
return NSURLSession.init(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: self, delegateQueue: NSOperationQueue.mainQueue())
}
获取NSURLSessionDataTask
需要手动开始
let dataTask = session.dataTaskWithRequest(request)
dataTask.resume()
下面是用到的代理方法
收到响应。需要注意的是需要把NSURLSessionResponseDisposition
设为Allow。
// 收到响应
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void)
这个方法会多次调用,在这里把收到的数据存到本地,使用NSOutputStream
可以实现拼接数据到本地。这里需要注意一下,OC中默认使用了多线程,在swift中默认在主线程执行
// 获取data 会多次调用
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData)
下载成功或者失败会调用,在这里关闭NSOutputStream
// 下载完成
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)
多任务断点下载思路
既然有多个任务要么数组要么字典,但是在这里使用数组没有字典容易控制数据,我们需要创建一个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: ((progerssValue: Float) -> Void)?, state: ((state: LCDownloadState) -> Void)?) {
let fileLength = getFileDataDownloadedLength(tag)
let allLength = getAllLength(tag)
if fileLength > 0 && fileLength == allLength {
state?(state: .Completed)
progerss?(progerssValue: 1.0)
return
}
let tagStr = String(tag)
if let download = downloadDic[tagStr] {
let dataTask = download.dataTask
if resume {
dataTask?.resume()
download.stateBlock?(state: .Running)
}else {
dataTask?.suspend()
download.stateBlock?(state: .Suspended)
}
}else {
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
let session = initSession()
request.setValue("bytes=\(fileLength)-", forHTTPHeaderField: "Range")
let dataTask = session.dataTaskWithRequest(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: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
let response = response as! NSHTTPURLResponse
let allLength = response.expectedContentLength + getFileDataDownloadedLength(dataTask.taskIdentifier)
print("--------------")
print(response.expectedContentLength)
setAllLength(allLength, WithTag: dataTask.taskIdentifier)
if let download = downloadDic[String(dataTask.taskIdentifier)] {
let path = initFileDataCachePath(dataTask.taskIdentifier)
print(path)
download.outputStream = NSOutputStream.init(toFileAtPath: path, append: true)
download.outputStream!.open()
download.allLength = Int(allLength)
}
completionHandler(.Allow)
}
在这里把获取的数据保存到沙盒,并且根据本地下载数据获取下载比例,UnsafePointer
是swift中的指针,用的很少。
// 获取data 会多次调用
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print(NSThread.currentThread())
if let download = downloadDic[String(dataTask.taskIdentifier)] {
let downloadedLength = getFileDataDownloadedLength(dataTask.taskIdentifier)
download.outputStream!.write(UnsafePointer<UInt8>(data.bytes), maxLength: data.length)
print(downloadedLength)
let progress = CGFloat(downloadedLength) / CGFloat(download.allLength)
download.stateBlock?(state: .Running)
download.progressBlock?(progress: progress)
}
}
下载完成关闭outputStream并且删除完成的model
// 下载完成
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
print("finish")
if let download = downloadDic[String(task.taskIdentifier)] {
download.stateBlock?(state: .Completed)
download.progressBlock?(progress: 1.0)
download.outputStream?.close()
download.outputStream = nil
downloadDic.removeValueForKey(String(task.taskIdentifier))
if error != nil {
download.stateBlock?(state: .Failed)
}
}
}
以上是主要的思路,在这里下载代码代码中包括OC和Swift,有写地方需要优化的地方希望大家指出