NSURLSession實現多任務斷點下載

在處理大文件的時候,我們不可能只是單一的去下載,那么我們就需要用到斷點下載,當然你可以使用第三方實現斷點下載,但是我們有時也要知道系統自帶的怎么用,萬一你使用的第三方不維護了,就麻煩了,當然這種概率很小。今天小編主要介紹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,
如果NSHTTPURLResponseexpectedContentLength = -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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容