iOS的下載功能實現

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

image.jpeg

當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/

http://www.lxweimin.com/p/1211cf99dfc3

http://www.lxweimin.com/p/2ccb34c460fd

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容