原博客連接html
當初編寫 0.x 版本時, 還沒有考慮過多邏輯, 總體架構就是簡單的封裝系統邏輯, 致使在後期頻頻出問題, 而打補丁只會出更多的問題, 畢竟底子並無打好, 因此就起了重構的心思.git
剛一開始想參考一下 aria2
這個大佬級下載軟件, 但奈何這軟件涉及的下載方式過於複雜, 並且 iOS 的封閉式致使大部分邏輯不能使用, 強行借鑑是沒有好下場的, 只好另做打算.github
以前由於須要爲本身的 App 編寫後臺, 就接觸到了 Python, 當時爲了將一些資源整合到本身的數據庫中, 就發現了 Scrapy
這個大名鼎鼎的爬蟲框架, 而這個框架的邏輯剛好符合個人想法.數據庫
根據 iOS 下載系統的特性, 下載框架會按下圖的流程進行製做: 後端
至於爲何要這麼作, 主要仍是得看製做一個後臺下載框架都有什麼難點.瀏覽器
下載任務和下載流程所有歸系統管理 首先, iOS 後臺下載的主要流程基本爲: 建立 NSURL
-> 建立 NSURLRequest
-> 使用 Session
和 NSURLRequest
生成 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
後, 不知道對應那個下載連接, 這種狀況的解決方案有幾種, 能夠監聽 NSURLSessionDownloadTask
的 currentRequest
屬性來記錄最終下載連接, 也可使用 taskDescription
屬性放置一個標記, 固然, 也許還有其餘方法.
獲取任務進度的自由度 對於 NSURLSessionDownloadTask
, 有 countOfBytesReceived
和 countOfBytesExpectedToReceive
這兩個屬性能夠獲取已下載字節長度和預計所有數據長度, 還有一些新的屬性也能夠獲取這些信息.
顯然, 對於單一任務來講, 這已經足夠了, 但對於多個任務, 而且能夠任意分組的狀況來講, 那就須要一個統一記錄進度的模塊了.
奇奇怪怪得系統 BUG 這些個 BUG 雖然都有應對方法, 但部分應對方法會影響一些邏輯.
好比在 iOS 12/12.1, iPhone 8
如下機型會出現 NSURLSessionDownloadTask
的 countOfBytesReceived
和 countOfBytesExpectedToReceive
屬性在進入後臺, 再回到前臺後, 再也不變動數值的問題, 須要來一個暫停/恢復的改變纔會繼續工做, 這就須要框架有一個監聽前/後臺切換的邏輯, 去處理這個問題.
還有那個恢復數據有問題致使恢復下載失敗的問題, 須要在每次恢復下載時作一次修復處理, 對於一些須要將恢復數據保存到本地的邏輯來講, 就須要特殊邏輯來使這些邏輯共存.
對於 Apple 來講, 後臺下載這些邏輯明顯是不想讓開發者過多幹涉, 因此在出現一些問題時, 開發者很被動, 只能找着法子各類規避了.
任務列表 每一個帶有下載功能的 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
是儲存.
首先, 是公開模塊.
該模塊主要對應 建立NSURLRequest
階段, 獨立出來是爲了更好的控制構建 NSURLReqest
的過程.
鑑於 NSURLReqest 的屬性和方法會隨着系統更新變得愈來愈多, 愈來愈複雜, 且自定義不會對生成 NSURLSessionDownloadTask
流程產生影響, 因此 FKBuilder
直接繼承於 NSMutableURLRequest
, 用戶能夠像操做 NSMutableURLRequest
同樣操做 FKBuilder
. 對於用戶傳入的 URL 是否合法, 也可在初始化時進行校驗.
FKBuilder
須要顯式執行預處理, 這樣才能將任務加入隊列中.
該模塊對應請求信息收集邏輯, 好比進度、狀態、錯誤等.
基本上全部有關下載的業務邏輯, 添加下載的界面和查看下載進度的是分開的, 因此, FKDownloader 就乾脆將任務信息收集相關邏輯所有獨立出來, 同時也能夠更好的對應列表樣式的信息獲取.
一個下載框架若是不能自定義配置, 那就沒有靈魂.
查看 NSURLSessionConfiguration
的官方文檔就會發現系統提供的參數巨多, 並且還包含了新版本特性, 這就致使對外開放什麼屬性/方法成了難題, 多了不可控, 少了又沒有高度自定義那味, 因此 FKConfigure
直接提供了一個 NSURLSessionConfiguration
模版, 化爲屬性, 只把其中的可否使用蜂窩 allowsCellularAccess
默認設置爲容許, 其餘統統由用戶自定義.
至於 NSMutableURLRequest
和 NSURLSessionConfiguration
的部分屬性衝突, 這部分官方已經在註釋裏講的很明白了, 用戶可自行斟酌.
這就是個控制任務的, 激活、暫停、繼續、中止、刪除, 沒什麼好說的, 獨立出來只是爲了面向用戶, 不用跟其餘私有模塊產生衝突, 並且多出一層, 可操做性也會多一層.
中間件模塊, 我也就在爬蟲框架和後端框架中見過, FKDownloader
中有這種模塊主要是爲了有一種能夠統一處理的方式.
目前該模塊只在生成 NSURLSessionDownloadTask
前, 下載成功/失敗後會有介入, 前者是爲了諸如請求統一加簽、繞過瀏覽器限制等操做, 後者能夠當成 NSURLSession
代理中下載成功/失敗的回調便可.
更多的操做可根據業務自行調整.
再來是私有模塊.
該模塊基本就是框架運轉的核心.
在通常下載邏輯中, 會爲每一個任務建立一個計時器, 實現進度信息分發邏輯, 但這一起其實用不了那麼多, 還會由於管理不過來出現問題(0.x版本就有這類問題), 因此在 FKEngine
中只有兩個個計時器, 只爲了更簡潔的操做, 畢竟在實際業務中, 這些進度條各走各仍是一塊兒走都無所謂,
計時器主要完成如下任務:
首先, FKBuilder
的預處理會將生成的任務信息模型(FKCacheRequestModel
) 存入 FKCache
的緩存隊列中, 但不開始任務, 也不建立 NSURLSessionDownloadTask
, 這是前提, 與計時器無關.
而後, 計時器被觸發後就會檢查隊列中正在執行的任務是否超過設置, 超過了就什麼也不作, 沒超過就開始用任務信息建立 NSURLSessionDownloadTask
, 進行下載, 期間走過中間件流程, 本地信息緩存流程, 監聽信息流程等等流程.
執行任務計時器的間隔爲 1s, 不可自定義.
在使用 FKMessager
時, 回調會被緩存, 計時器被觸發後將會輪詢執行這些回調
以上任務都會在計時器觸發後執行. 默認狀況下, 這個計時器是中止的, 須要用 FKConfigure
來激活, 這是爲了保證在 NSURLSession
被建立後再執行任務.
計時器重複時間默認設定爲 1s, 這個時間剛恰好, 少了太頻繁, 多了感受慢. 也支持用戶自定義速率, 目前 1 倍速率爲 0.2 秒, 倍率在 1 ~ 10 倍區間可自定義.
除此以外, 應用啓動後還會去查詢後臺已經存在的下載任務, 將這些任務添加到緩存中, 讓它們和其餘任務一致.
基本上其餘模塊只是製造信息, 輸出信息和保存信息, 而 FKEngine
則是讓整個框架活過來.
主要負責信息緩存, 任務的信息, 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版本中都是全部任務都在一個文件中, 無論從性能上看, 仍是管理上看, 都有很大的問題.
衆多任務須要監聽的流程太過繁雜也太過度散, 系統 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
配合完美, 一個對外, 一個對內.
FKBuilder
和 FKEngine
之間的模塊, FKControl
的實現, 主要任務以下:
FKBuilder
的預處理邏輯進行了更細節的處理. 如建立任務信息文件, 添加內存/本地緩存, 忽略已存在任務等.FKControl
的操做, 激活、暫停、繼續、取消、刪除.實現 NSURLSession
的代理, 沒啥好說, 單獨摘出來是由於代理方法仍是不少很複雜的, 爲了以後更好的擴展, 這樣更好一些.
還有一些輔助用模塊
在執行下一個任務時, 哪一個纔是下一個? 按添加順序可不必定準, 因此 FKDownloader
直接使用 stdatomic.h
中的 atomic_ullong
來建立一個不受線程影響的原子數, 再讓它被獲取時自增.
FKCacheRequestModel
的 requestSingleID
就是原子數和下載連接的哈希值拼接出來的.
固然, 從業務上來說, 任務的執行順序是否按照列表所示順序依次進行好像並不怎麼重要.
文件管理的封裝, 主要負責建立/刪除任務對應的文件/文件夾.
輔助信息日誌, 這卻是沒啥好說的, 只是爲了更方便調試, 信息只會在 DEBUG 環境下執行.
URL 編解碼.
先說編碼, 有一個問題即是用戶傳入的下載連接是否已經編碼過, 這個能夠循環解碼至和上一個結果一致時停下, 這個問題不算太大. 但 URL 可能有帶有 emoji
, fragment
的狀況, emoji
能夠用系統的 URLQueryAllowedCharacterSet
直接處理, 但 fragment
就會編碼錯誤, 這時就須要分段處理.
再說解碼, 這個就簡單了, 直接 stringByRemovingPercentEncoding
走起.
既然是文件下載, 那基本上都有後綴吧, 直接從 URL 裏獲取是不現實的, 畢竟有的連接是加簽的, 後綴是不存在的, 因此要用 Response
中的 Content-Type
也就是 MIMEType
來轉爲後綴名.
系統能夠講 MIMEType
轉爲後綴, 但並不全面, 因此須要將其餘經常使用的加入轉換列表中, 若是實在沒有對應後綴名, 就用 unknown
爲後綴名.
後臺下載功能中也存在一些須要知道的東西.
先說結論, 目前沒有什麼好的方法去繞過. 系統的限制基本上有如下幾種:
總的來講, iOS 爲了達到完美的運行而且不會影響系統的穩定性, 後臺下載的內核作了很是多的限制, 並且爲何有這些限制, 又是怎樣作到的, 官方並無明說, 只能從這裏看出一個重點信息, NSURLSession Background Download 是系統包攬的, 開發者最好不要深刻研究.
從這裏能夠看出, 測試時必定要嚴格遵照如下幾點: