1,首先要確定下載好的文件放在哪里
放在緩存目錄( NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0])是不行的,因為系統不知道什么時候就會把緩存給清了,這可能會導致用戶下載的文件丟失。
所以應該放在用戶的document目錄,也就是 NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0];
可以在這個目錄下新建一個xxx.xxx.download文件夾,作為下載目錄。
這里還需要注意: iOS在備份的時候會默認備份放在document目錄下面的文件,而一般來說,可下載的內容一般是不需要備份的,
可以手動設置這個下載目錄為不需要備份
do {
try kOJSFileManager.createDirectory(atPath: kWHGSaveDirectoryPath, withIntermediateDirectories: false, attributes: nil)
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
var url = URL(fileURLWithPath: kWHGSaveDirectoryPath)
try url.setResourceValues(resourceValues)
} catch {
NSLog(error)
}
2,確定下載所用的網絡連接類
其實沒啥好選的, iOS7以后已經全面開始推廣URLSession了,而且URLSession功能確實很強大,這里需要確定是,是用AFNetworking又封裝了一層的AFHTTPSessionManager呢,還是用系統自帶的URLSession,區別是AFNetworking提供了block和delegate的回調,而系統的只有delegate的回調方式。這個看個人喜好了,我自己是更傾向于用AFNetworking的,畢竟簡單好用才是硬道理。
然后看 URLSessionConfiguration,
open class var `default`: URLSessionConfiguration { get }
open class var ephemeral: URLSessionConfiguration { get }
@available(iOS 8.0, *)
open class func background(withIdentifier identifier: String) -> URLSessionConfiguration
因為要支持后臺下載,所以只能用background,這里需要一個identifier,用來標識session,當重新啟動應用時,如果創建和之前有相同identifier的session,系統會找到對應的session數據,并響應-URLSession: task: didCompleteWithError:方法
3,NSURLSessionTask
一個下載任務對應一個task,一個URLSession可以對應多個task,而NSURLSessionTask是一個基類,有四個子類:
1)NSURLSessionDataTask,這是一般的網絡請求用的類,不支持后臺傳輸,切換后臺會終止下載。AFNetworking的get,post方法都是用的這個類
有幾點需要注意,調用cancel方法會立即進入-URLSession: task: didCompleteWithError這個回調;調用suspend方法,即使任務已經暫停,但達到超時時長,也會進入這個回調,可以通過error進行判斷;當一個任務調用了resume方法,但還未開始接受數據,這時調用suspend方法是無效的。也可以通過cancel方法實現暫停,只是每次需要重新創建NSURLSessionDataTask。
2)NSURLSessionUploadTask,繼承自NSURLSessionDataTask,內容以NSData對象返回,協議方法中可以查看請求時上傳內容的過程,支持后臺傳輸。
3)NSURLSessionStreamTask,建立了一個TCP/IP連接,替代NSInputStream/NSOutputStream,新的API可異步讀寫,自動通過HTTP代理連接遠程服務器。
4)NSURLSessionDownloadTask,支持斷點續傳,資源會下載到一個臨時文件,下載完成需將文件移動至想要的路徑,系統會刪除臨時路勁文件,暫停時,系統會返回NSData對象,恢復下載時用這個data創建task,支持后臺傳輸。AFNetworking的下載方法也是用這個類。
downloadTask有兩種創建方式,用resumeData就能實現斷點續傳,直接用request就是從頭開始下載。
用 open func cancel(byProducingResumeData completionHandler: @escaping (Data?) -> Void)方法即可產生resumeData。
4,具體下載方法就比較簡單了,我這里直接用AFNetworking的download方法。
5,下載的model
把要下載的數據定義為一個model,這個model包含下載的url,保存地址,唯一標志符,下載狀態,目標文件大小,已下載的大小等。
其中下載狀態包括{none, downloading, waiting, paused, finished, failed}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
綜上所述,大體的結構應該是這樣:
有一個downloadManager來管理下載相關事務,
它有一個downloadSession來創建下載任務,下載指定類型的model;
有一個dataArray來保存各個狀態的model,可以用只讀的方法分成finishedArray,downloadingArray等;
這個dataArray應該是可以保存到本地的,不管是用序列化還是數據庫,重啟的時候應該可以重新拿到這些內容;
需要實現downloadSession的各種回調。
5,下載進度更新
首先將下載進度保存到下載的model當中,然后可以用通知或者kvo的方式來實時改變進度條。
這里稍微注意一下,讓ViewController來做observer是不太合適的,下載進度更新很快,每次都在ViewController讓tableView reloadData顯然是不行的,
所以還是應該讓cell自己來監聽,而cell是復用的,配置cell用的數據可以一開始是model1,滑動幾下就變成model2,model3了,
用通知的話,要判斷發送通知的下載任務的model和當然配置cell的model是同一個model才可以更新;
用kvo的話,在要配置cell時,移除對原來model的監聽,添加對新model的監聽。
6,允許蜂窩網絡下載
NSURLSessionConfiguration本身就有一個屬性allowsCellularAccess,默認為YES,允許蜂窩網絡下載。但是對于正在下載的任務,修改這個屬性是無效的,即我們已經通過session創建了task對象,開啟了任務,再試圖用session.configuration.allowsCellularAccess = NO;去修改這個選項是無效的。如果一定要用這個屬性修改這個選項,那么只能重新創建session。
所以如果想達到設置屬性或者切換網絡就暫停下載的效果,還是需要自定義一個allowsCellularAccess屬性,在設置屬性的時候,手動暫停正在下載的任務
7,支持后臺下載
支持后臺下載,首先要用background的URLConfigSession,
然后在AppDelegate中實現方法:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)
在其中保存completionHandler這個block;
然后在URLSession的delegate方法
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
中,調用這個block;
借用別人的流程圖:(http://www.lxweimin.com/p/1211cf99dfc3)
當APP進入后臺時,系統會接管這個background的URLSession,當下載任務完成時,系統會調用AppDelegate的handleEventsForBackgroundURLSession方法,
關于completionHandler的作用,可以參考http://www.lxweimin.com/p/2ccb34c460fd
8,斷點續傳
斷點續傳的功能URLSession已經封裝實現了,就是靠resumeData,這個resumeData并不是真正的已經下載的文件,它只是一個描述已下載文件的描述文件,相當于用迅雷下載的時候產生的cfg文件,真正的下載中的文件應該是系統下載到臨時文件夾了,下載完成后會將文件從臨時文件夾移動到指定的下載目錄。
需要注意的是iOS10,iOS10.1系統中(也有說法是iOS11和iOS11.1),downloadTaskWithResumeData方法有問題,需要特殊處理,參考https://benscheirman.com/2016/09/resume-data-broken-in-ios-10/,在iOS11.2之后的版本應該是已經修復了。
注意:如果想要APP在后臺被kill掉時依然保存resumeData,那就不能使用AFNetworking的download方法中的那個幾個block,因為AFNetworking文檔有提到,這些block可能會丟失(因為是保存在內存里的嘛,沒有本地化,當APP被kill掉時就沒了),只能用delegate的方式,或者用session的 setXXXBlock方法,這些方法在session重新生成的時候會重新調用,所以不存在丟失的問題。
注意:正在下載的過程中,手動kill掉APP,這時候是拿不到resumeData的,resumeData是系統保存起來的,當APP再次啟動,重新創建了跟原來backgroundSession的identifier一致的session時,系統會調用這個session的delegate方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
這個error的userInfo中,會有resumeData和task的url等信息。
在APP willTerminate的通知里調用cancelDownloadByProducingResumeData也是是不行的,因為這個方法是異步的,需要執行時間的,可能還沒執行完APP就已經被kill了
另外,貌似直接調用task的resume是可行,會直接開始斷點續傳,但這種操作應該不是用戶希望看到的。所以還是保存下來resumeData,將任務設置為paused,等用戶手動開啟比較好。
還有,如果APP被kill了,那delegate方法中拿到的task,跟原來開始的task不是同一個對象,但是它們所有屬性都相同(我也沒有一個一個挨個看,task的指針地址是不一樣的,但是taskIdentifier,taskDescription,currentRequest等都是相同的)。這里尤其需要注意的是,如果用了AFNetworking,AFURLSession在初始化的時候,會調用getTasksWithCompletionHandler
方法
再其block回調中會調用addDelegateForDownloadDataTask
方法,這個方法會修改task的taskDescription,所以就算之前自己手動修改了task的taskDescription,重啟后獲取的還是會不一樣。。。這個稍微有點坑!!!
還有一個特別容易坑的地方,也是使用AFNetworking才會有的。就是didCompleteWithError方法調用時機的問題,AFURLSession內部是用delegate方式實現回調的,提供給外部block的回調方式。而didCompleteWithError方法會在session初始化的時候( + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;
)就調用,(目測應該是在設置delegate后的下一個runloop或幾個runloop之內)因為session初始化后對session設置block回調是可以的,打斷點觀察delegate方法的調用也在getTasksWithCompletionHandler
方法的回調之后,可能是新建delegateQueue需要一點時間。總之,如果在session初始化后沒有立刻給session設置block回調,而是執行了某些可能會耗時的操作(比如io讀寫之類),那可能在設置block回調之前delegate方法就被調用了,從而導致回調block沒有沒觸發!!!AFURLSession內部是用delegate的方式,所以不存在這個問題~
9,下載鏈接失效的問題
如果下載鏈接失效了,繼續用這個鏈接下載的話,就會觸發didCompleteWithError方法但是error可能為nil,這時候貌似只能用task.response的statusCode來判斷,注意!!!
參考:
https://blog.csdn.net/hero_wqb/article/details/80407478
https://benscheirman.com/2016/09/resume-data-broken-in-ios-10/