對 FKDownloader 的徹底重構

原博客連接html

前言

當初編寫 0.x 版本時, 還沒有考慮過多邏輯, 總體架構就是簡單的封裝系統邏輯, 致使在後期頻頻出問題, 而打補丁只會出更多的問題, 畢竟底子並無打好, 因此就起了重構的心思.git

靈感

剛一開始想參考一下 aria2 這個大佬級下載軟件, 但奈何這軟件涉及的下載方式過於複雜, 並且 iOS 的封閉式致使大部分邏輯不能使用, 強行借鑑是沒有好下場的, 只好另做打算.github

以前由於須要爲本身的 App 編寫後臺, 就接觸到了 Python, 當時爲了將一些資源整合到本身的數據庫中, 就發現了 Scrapy 這個大名鼎鼎的爬蟲框架, 而這個框架的邏輯剛好符合個人想法.數據庫

根據 iOS 下載系統的特性, 下載框架會按下圖的流程進行製做: 後端

至於爲何要這麼作, 主要仍是得看製做一個後臺下載框架都有什麼難點.瀏覽器

後臺下載框架主要難點與關鍵點

  1. 下載任務和下載流程所有歸系統管理 首先, iOS 後臺下載的主要流程基本爲: 建立 NSURL -> 建立 NSURLRequest -> 使用 SessionNSURLRequest 生成 NSURLSessionDownloadTask, 以後的下載信息, 以下載進度, 下載失敗和下載成功等事件能夠經過設置代理後得到.緩存

    而後, 如何在重啓 App 後得到下載任務? 能夠經過 -[NSURLSession getTasksWithCompletionHandler:] 獲取.bash

    但這個方法真的能獲取全部請求嗎? 官方文檔有解釋:session

    The arrays passed to the completion handler contain any tasks that you have created within the session, not including any tasks that have been invalidated after completing, failing, or being cancelled.數據結構

    顯而易見, 對於已經失效的任務是獲取不到的, 因此擁有一套任務記錄模塊是必須的.

    還有一種狀況, 連接重定向, 這會致使拿到 NSURLSessionDownloadTask 後, 不知道對應那個下載連接, 這種狀況的解決方案有幾種, 能夠監聽 NSURLSessionDownloadTaskcurrentRequest 屬性來記錄最終下載連接, 也可使用 taskDescription 屬性放置一個標記, 固然, 也許還有其餘方法.

  2. 獲取任務進度的自由度 對於 NSURLSessionDownloadTask, 有 countOfBytesReceivedcountOfBytesExpectedToReceive 這兩個屬性能夠獲取已下載字節長度和預計所有數據長度, 還有一些新的屬性也能夠獲取這些信息.

    顯然, 對於單一任務來講, 這已經足夠了, 但對於多個任務, 而且能夠任意分組的狀況來講, 那就須要一個統一記錄進度的模塊了.

  3. 奇奇怪怪得系統 BUG 這些個 BUG 雖然都有應對方法, 但部分應對方法會影響一些邏輯.

    好比在 iOS 12/12.1, iPhone 8 如下機型會出現 NSURLSessionDownloadTaskcountOfBytesReceivedcountOfBytesExpectedToReceive 屬性在進入後臺, 再回到前臺後, 再也不變動數值的問題, 須要來一個暫停/恢復的改變纔會繼續工做, 這就須要框架有一個監聽前/後臺切換的邏輯, 去處理這個問題.

    還有那個恢復數據有問題致使恢復下載失敗的問題, 須要在每次恢復下載時作一次修復處理, 對於一些須要將恢復數據保存到本地的邏輯來講, 就須要特殊邏輯來使這些邏輯共存.

    對於 Apple 來講, 後臺下載這些邏輯明顯是不想讓開發者過多幹涉, 因此在出現一些問題時, 開發者很被動, 只能找着法子各類規避了.

  4. 任務列表 每一個帶有下載功能的 App 基本上都會有下載列表, 但在下載框架裏, 最好不要現實下載列表功能, 這畢竟屬於 App 的業務邏輯.

    並且由於業務影響, UI 影響, 會致使數據結構千奇百怪, 第三方框架不必兼容, 也兼容不了.

    因此任務信息相關的模塊最好保持懶加載模式, 須要的時候直接拿來用, UI 相關的數據只保留最基本的數據, 其餘信息能夠由 App 端實現.

框架模塊

接下來就是根據架構流程圖建立各類模塊了, 在 FKDownloader 中有兩種類型的模塊, 一種公開類型, 一種私有類型.

公開模塊爲面向用戶的模塊, 包含 FKBuilder(構建任務), FKMessager(獲取信息), FKConfigure(下載配置), FKControl(控制任務), FKMiddleware(中間件) 五個模塊.

私有模塊爲框架內部使用模塊, 包含主要模塊: FKEngine, FKCache, FKObserver, FKScheduler, FKSessionDelegater.

還有輔助模塊: FKSingleNumber, FKFileManager, FKLogger, FKCoder, FKMIMEType.

還有一些數據模型: FKObserverModel, FKCacheRequestModel, FKResponse.

它們將按照流程圖來處理下載任務, 大致上來說, FKBuilder 是輸入, FKMessager 是輸出, FKEngine 是處理, FKControl 是控制, FKCache 是儲存.

框架模塊詳細講解

首先, 是公開模塊.

FKBuilder

該模塊主要對應 建立NSURLRequest 階段, 獨立出來是爲了更好的控制構建 NSURLReqest 的過程.

鑑於 NSURLReqest 的屬性和方法會隨着系統更新變得愈來愈多, 愈來愈複雜, 且自定義不會對生成 NSURLSessionDownloadTask 流程產生影響, 因此 FKBuilder 直接繼承於 NSMutableURLRequest, 用戶能夠像操做 NSMutableURLRequest 同樣操做 FKBuilder. 對於用戶傳入的 URL 是否合法, 也可在初始化時進行校驗.

FKBuilder 須要顯式執行預處理, 這樣才能將任務加入隊列中.

FKMessager

該模塊對應請求信息收集邏輯, 好比進度、狀態、錯誤等.

基本上全部有關下載的業務邏輯, 添加下載的界面和查看下載進度的是分開的, 因此, FKDownloader 就乾脆將任務信息收集相關邏輯所有獨立出來, 同時也能夠更好的對應列表樣式的信息獲取.

FKConfigure

一個下載框架若是不能自定義配置, 那就沒有靈魂.

查看 NSURLSessionConfiguration 的官方文檔就會發現系統提供的參數巨多, 並且還包含了新版本特性, 這就致使對外開放什麼屬性/方法成了難題, 多了不可控, 少了又沒有高度自定義那味, 因此 FKConfigure 直接提供了一個 NSURLSessionConfiguration 模版, 化爲屬性, 只把其中的可否使用蜂窩 allowsCellularAccess 默認設置爲容許, 其餘統統由用戶自定義.

至於 NSMutableURLRequestNSURLSessionConfiguration 的部分屬性衝突, 這部分官方已經在註釋裏講的很明白了, 用戶可自行斟酌.

FKControl

這就是個控制任務的, 激活、暫停、繼續、中止、刪除, 沒什麼好說的, 獨立出來只是爲了面向用戶, 不用跟其餘私有模塊產生衝突, 並且多出一層, 可操做性也會多一層.

FKMiddleware

中間件模塊, 我也就在爬蟲框架和後端框架中見過, FKDownloader 中有這種模塊主要是爲了有一種能夠統一處理的方式.

目前該模塊只在生成 NSURLSessionDownloadTask 前, 下載成功/失敗後會有介入, 前者是爲了諸如請求統一加簽、繞過瀏覽器限制等操做, 後者能夠當成 NSURLSession 代理中下載成功/失敗的回調便可.

更多的操做可根據業務自行調整.



再來是私有模塊.

FKEngine

該模塊基本就是框架運轉的核心.

在通常下載邏輯中, 會爲每一個任務建立一個計時器, 實現進度信息分發邏輯, 但這一起其實用不了那麼多, 還會由於管理不過來出現問題(0.x版本就有這類問題), 因此在 FKEngine 中只有兩個個計時器, 只爲了更簡潔的操做, 畢竟在實際業務中, 這些進度條各走各仍是一塊兒走都無所謂,

計時器主要完成如下任務:

  1. 檢查任務隊列, 進行下一個任務

首先, FKBuilder 的預處理會將生成的任務信息模型(FKCacheRequestModel) 存入 FKCache 的緩存隊列中, 但不開始任務, 也不建立 NSURLSessionDownloadTask, 這是前提, 與計時器無關.

而後, 計時器被觸發後就會檢查隊列中正在執行的任務是否超過設置, 超過了就什麼也不作, 沒超過就開始用任務信息建立 NSURLSessionDownloadTask, 進行下載, 期間走過中間件流程, 本地信息緩存流程, 監聽信息流程等等流程.

執行任務計時器的間隔爲 1s, 不可自定義.

  1. 分發任務信息

在使用 FKMessager 時, 回調會被緩存, 計時器被觸發後將會輪詢執行這些回調

以上任務都會在計時器觸發後執行. 默認狀況下, 這個計時器是中止的, 須要用 FKConfigure 來激活, 這是爲了保證在 NSURLSession 被建立後再執行任務.

計時器重複時間默認設定爲 1s, 這個時間剛恰好, 少了太頻繁, 多了感受慢. 也支持用戶自定義速率, 目前 1 倍速率爲 0.2 秒, 倍率在 1 ~ 10 倍區間可自定義.

除此以外, 應用啓動後還會去查詢後臺已經存在的下載任務, 將這些任務添加到緩存中, 讓它們和其餘任務一致.

基本上其餘模塊只是製造信息, 輸出信息和保存信息, 而 FKEngine 則是讓整個框架活過來.

FKCache

主要負責信息緩存, 任務的信息, NSURLSessionDownloadTask 等等.

獨立的緩存模塊是必須的, 要緩存的信息有不少, 集中起來更容易管理.

FKDownloader 中, 對任務有一個主要理念: 任務即文件, 文件即任務, 每個任務都有一個屬於本身的惟一標識, 標識與用戶輸入的連接息息相關, 也和本地緩存有着千絲萬縷的聯繫.

FKDownloader 中的文件就是 FKCacheRequestModel 對應的歸檔文件, 這個模型提供的信息有:

@property (nonatomic, strong) NSString *requestID; // 請求標識, SHA256(URL)
@property (nonatomic, strong) NSString *requestSingleID; // 惟一請求標識, SingleNumber_SHA256(URL)
@property (nonatomic, strong) NSString *idx; // 惟一順序編碼
@property (nonatomic, strong) NSString *url; // 原始請求連接
@property (nonatomic, strong) NSMutableURLRequest *request; // 請求
@property (nonatomic, assign) FKState state; // 請求狀態
@property (nonatomic, assign) int64_t receivedLength; // 接收的數據長度
@property (nonatomic, assign) int64_t dataLength; // 數據長度
@property (nonatomic, strong) NSString *extension; // 文件後綴, `.*`
@property (nonatomic, strong, nullable) NSData *resumeData; // 恢復數據
@property (nonatomic, strong, nullable) NSError *error; // 錯誤
複製代碼

基本上能夠構成/恢復任務的信息都在裏面, 每個任務都有本身的文件夾保存這些信息, 分而治之有利於管理, 0.x版本中都是全部任務都在一個文件中, 無論從性能上看, 仍是管理上看, 都有很大的問題.

FKObserver

衆多任務須要監聽的流程太過繁雜也太過度散, 系統 BUG 還致使這些監聽還須要從新添加, 這就更分散了. 而獲取進度信息在業務上來說並不頻繁使用, 這些監聽到的信息全放在任務信息模型裏也不合適, 那麼, 直接獨立出來成模塊豈不美哉.

FKObserver 以專門監聽 NSURLSessionDownloadTask 而生, 全部任務的進度信息都在這裏.

FKObserver 使用 FKObserverModel 保存進度信息, 基本信息以下:

@interface FKObserverModel : NSObject

@property (nonatomic, strong) NSString *requestID; // SHA256(Request.URL)
@property (nonatomic, assign) int64_t countOfBytesReceived;
@property (nonatomic, assign) int64_t countOfBytesPreviousReceived;
@property (nonatomic, assign) int64_t countOfBytesExpectedToReceive;

@end
複製代碼

簡約而不簡單, 而且和 FKMessager 配合完美, 一個對外, 一個對內.

FKScheduler

FKBuilderFKEngine 之間的模塊, FKControl 的實現, 主要任務以下:

  1. FKBuilder 的預處理邏輯進行了更細節的處理. 如建立任務信息文件, 添加內存/本地緩存, 忽略已存在任務等.
  2. 實現 FKControl 的操做, 激活、暫停、繼續、取消、刪除.
FKSessionDelegater

實現 NSURLSession 的代理, 沒啥好說, 單獨摘出來是由於代理方法仍是不少很複雜的, 爲了以後更好的擴展, 這樣更好一些.



還有一些輔助用模塊

FKSingleNumber

在執行下一個任務時, 哪一個纔是下一個? 按添加順序可不必定準, 因此 FKDownloader 直接使用 stdatomic.h 中的 atomic_ullong 來建立一個不受線程影響的原子數, 再讓它被獲取時自增.

FKCacheRequestModelrequestSingleID 就是原子數和下載連接的哈希值拼接出來的.

固然, 從業務上來說, 任務的執行順序是否按照列表所示順序依次進行好像並不怎麼重要.

FKFileManager

文件管理的封裝, 主要負責建立/刪除任務對應的文件/文件夾.

FKLogger

輔助信息日誌, 這卻是沒啥好說的, 只是爲了更方便調試, 信息只會在 DEBUG 環境下執行.

FKCoder

URL 編解碼.

先說編碼, 有一個問題即是用戶傳入的下載連接是否已經編碼過, 這個能夠循環解碼至和上一個結果一致時停下, 這個問題不算太大. 但 URL 可能有帶有 emoji, fragment 的狀況, emoji 能夠用系統的 URLQueryAllowedCharacterSet 直接處理, 但 fragment 就會編碼錯誤, 這時就須要分段處理.

再說解碼, 這個就簡單了, 直接 stringByRemovingPercentEncoding 走起.

FKMIMEType.

既然是文件下載, 那基本上都有後綴吧, 直接從 URL 裏獲取是不現實的, 畢竟有的連接是加簽的, 後綴是不存在的, 因此要用 Response 中的 Content-Type 也就是 MIMEType 來轉爲後綴名.

系統能夠講 MIMEType 轉爲後綴, 但並不全面, 因此須要將其餘經常使用的加入轉換列表中, 若是實在沒有對應後綴名, 就用 unknown 爲後綴名.

關於後臺下載的 Tips

後臺下載功能中也存在一些須要知道的東西.

後臺任務由系統啓動後的各類限制

先說結論, 目前沒有什麼好的方法去繞過. 系統的限制基本上有如下幾種:

  1. 限制下載速度
  2. 限制什麼時候啓動下一個任務的時間
  3. 限制任務啓動數量
  4. ....

總的來講, iOS 爲了達到完美的運行而且不會影響系統的穩定性, 後臺下載的內核作了很是多的限制, 並且爲何有這些限制, 又是怎樣作到的, 官方並無明說, 只能從這裏看出一個重點信息, NSURLSession Background Download 是系統包攬的, 開發者最好不要深刻研究.

測試後臺下載流程

這裏能夠看出, 測試時必定要嚴格遵照如下幾點:

  1. Test on a real device, not the simulator. 在真機上測試, 而不是模擬器.
  2. Run your app from the Home screen rather than running it from Xcode. 從主屏幕上運行, 而不是 Xcode 直接運行.
  3. Do not use force quit to test the ‘relaunch in the background case’. 不要從任務管理中強制退出 App 來模擬後臺下載流程中的強制中斷 App 邏輯, 而是在合適的地方使用 exit() 來退出 App.

參考

  1. MIMEType IANA
  2. MIMEType to Extension
  3. NSURLSession’s Resume Rate Limiter
  4. Testing Background Session Code
相關文章
相關標籤/搜索