前言
當(dāng)初編寫 0.x 版本時(shí), 尚未考慮過多邏輯, 整體架構(gòu)就是簡單的封裝系統(tǒng)邏輯, 導(dǎo)致在后期頻頻出問題, 而打補(bǔ)丁只會出更多的問題, 畢竟底子并沒有打好, 所以就起了重構(gòu)的心思.
靈感
剛一開始想?yún)⒖家幌?aria2
這個(gè)大佬級下載軟件, 但奈何這軟件涉及的下載方式過于復(fù)雜, 而且 iOS 的封閉式導(dǎo)致大部分邏輯不能使用, 強(qiáng)行借鑒是沒有好下場的, 只好另作打算.
之前因?yàn)樾枰獮樽约旱?App 編寫后臺, 就接觸到了 Python, 當(dāng)時(shí)為了將一些資源整合到自己的數(shù)據(jù)庫中, 就發(fā)現(xiàn)了 Scrapy
這個(gè)大名鼎鼎的爬蟲框架, 而這個(gè)框架的邏輯恰好符合我的想法.
根據(jù) iOS 下載系統(tǒng)的特性, 下載框架會按下圖的流程進(jìn)行制作:
至于為什么要這么做, 主要還是得看制作一個(gè)后臺下載框架都有什么難點(diǎn).
后臺下載框架主要難點(diǎn)與關(guān)鍵點(diǎn)
-
下載任務(wù)和下載流程全部歸系統(tǒng)管理
首先, iOS 后臺下載的主要流程基本為: 創(chuàng)建NSURL
-> 創(chuàng)建NSURLRequest
-> 使用Session
和NSURLRequest
生成NSURLSessionDownloadTask
, 之后的下載信息, 如下載進(jìn)度, 下載失敗和下載成功等事件可以通過設(shè)置代理后獲得.然后, 如何在重啟 App 后獲得下載任務(wù)? 可以通過
-[NSURLSession getTasksWithCompletionHandler:]
獲取.但這個(gè)方法真的能獲取所有請求嗎? 官方文檔有解釋:
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.
顯而易見, 對于已經(jīng)失效的任務(wù)是獲取不到的, 所以擁有一套任務(wù)記錄模塊是必須的.
還有一種情況, 鏈接重定向, 這會導(dǎo)致拿到
NSURLSessionDownloadTask
后, 不知道對應(yīng)那個(gè)下載鏈接, 這種情況的解決方案有幾種, 可以監(jiān)聽NSURLSessionDownloadTask
的currentRequest
屬性來記錄最終下載鏈接, 也可以使用taskDescription
屬性放置一個(gè)標(biāo)記, 當(dāng)然, 也許還有其他方法. -
獲取任務(wù)進(jìn)度的自由度
對于NSURLSessionDownloadTask
, 有countOfBytesReceived
和countOfBytesExpectedToReceive
這兩個(gè)屬性可以獲取已下載字節(jié)長度和預(yù)計(jì)全部數(shù)據(jù)長度, 還有一些新的屬性也可以獲取這些信息.顯然, 對于單一任務(wù)來說, 這已經(jīng)足夠了, 但對于多個(gè)任務(wù), 并且可以任意分組的情況來說, 那就需要一個(gè)統(tǒng)一記錄進(jìn)度的模塊了.
-
奇奇怪怪得系統(tǒng) BUG
這些個(gè) BUG 雖然都有應(yīng)對方法, 但部分應(yīng)對方法會影響一些邏輯.比如在
iOS 12/12.1, iPhone 8
以下機(jī)型會出現(xiàn)NSURLSessionDownloadTask
的countOfBytesReceived
和countOfBytesExpectedToReceive
屬性在進(jìn)入后臺, 再回到前臺后, 不再變更數(shù)值的問題, 需要來一個(gè)暫停/恢復(fù)的改變才會繼續(xù)工作, 這就需要框架有一個(gè)監(jiān)聽前/后臺切換的邏輯, 去處理這個(gè)問題.還有那個(gè)恢復(fù)數(shù)據(jù)有問題導(dǎo)致恢復(fù)下載失敗的問題, 需要在每次恢復(fù)下載時(shí)做一次修復(fù)處理, 對于一些需要將恢復(fù)數(shù)據(jù)保存到本地的邏輯來說, 就需要特殊邏輯來使這些邏輯共存.
對于 Apple 來說, 后臺下載這些邏輯明顯是不想讓開發(fā)者過多干涉, 所以在出現(xiàn)一些問題時(shí), 開發(fā)者很被動, 只能找著法子各種規(guī)避了.
-
任務(wù)列表
每個(gè)帶有下載功能的 App 基本上都會有下載列表, 但在下載框架里, 最好不要現(xiàn)實(shí)下載列表功能, 這畢竟屬于 App 的業(yè)務(wù)邏輯.而且因?yàn)闃I(yè)務(wù)影響, UI 影響, 會導(dǎo)致數(shù)據(jù)結(jié)構(gòu)千奇百怪, 第三方框架沒必要兼容, 也兼容不了.
所以任務(wù)信息相關(guān)的模塊最好保持懶加載模式, 需要的時(shí)候直接拿來用, UI 相關(guān)的數(shù)據(jù)只保留最基本的數(shù)據(jù), 其他信息可以由 App 端實(shí)現(xiàn).
框架模塊
接下來就是根據(jù)架構(gòu)流程圖創(chuàng)建各種模塊了, 在 FKDownloader
中有兩種類型的模塊, 一種公開類型, 一種私有類型.
公開模塊為面向用戶的模塊, 包含 FKBuilder
(構(gòu)建任務(wù)), FKMessager
(獲取信息), FKConfigure
(下載配置), FKControl
(控制任務(wù)), FKMiddleware
(中間件) 五個(gè)模塊.
私有模塊為框架內(nèi)部使用模塊, 包含主要模塊: FKEngine
, FKCache
, FKObserver
, FKScheduler
, FKSessionDelegater
.
還有輔助模塊: FKSingleNumber
, FKFileManager
, FKLogger
, FKCoder
, FKMIMEType
.
還有一些數(shù)據(jù)模型: FKObserverModel
, FKCacheRequestModel
, FKResponse
.
它們將按照流程圖來處理下載任務(wù), 大體上來講, FKBuilder
是輸入, FKMessager
是輸出, FKEngine
是處理, FKControl
是控制, FKCache
是儲存.
框架模塊詳細(xì)講解
首先, 是公開模塊.
FKBuilder
該模塊主要對應(yīng) 創(chuàng)建NSURLRequest
階段, 獨(dú)立出來是為了更好的控制構(gòu)建 NSURLReqest
的過程.
鑒于 NSURLReqest 的屬性和方法會隨著系統(tǒng)更新變得越來越多, 越來越復(fù)雜, 且自定義不會對生成 NSURLSessionDownloadTask
流程產(chǎn)生影響, 所以 FKBuilder
直接繼承于 NSMutableURLRequest
, 用戶可以像操作 NSMutableURLRequest
一樣操作 FKBuilder
. 對于用戶傳入的 URL 是否合法, 也可在初始化時(shí)進(jìn)行校驗(yàn).
FKBuilder
需要顯式執(zhí)行預(yù)處理, 這樣才能將任務(wù)加入隊(duì)列中.
FKMessager
該模塊對應(yīng)請求信息收集邏輯, 比如進(jìn)度、狀態(tài)绣溜、錯(cuò)誤等.
基本上所有有關(guān)下載的業(yè)務(wù)邏輯, 添加下載的界面和查看下載進(jìn)度的是分開的, 所以, FKDownloader 就干脆將任務(wù)信息收集相關(guān)邏輯全部獨(dú)立出來, 同時(shí)也可以更好的對應(yīng)列表樣式的信息獲取.
FKConfigure
一個(gè)下載框架如果不能自定義配置, 那就沒有靈魂.
查看 NSURLSessionConfiguration
的官方文檔就會發(fā)現(xiàn)系統(tǒng)提供的參數(shù)巨多, 而且還包含了新版本特性, 這就導(dǎo)致對外開放什么屬性/方法成了難題, 多了不可控, 少了又沒有高度自定義那味, 所以 FKConfigure
直接提供了一個(gè) NSURLSessionConfiguration
模版, 化為屬性, 只把其中的能否使用蜂窩 allowsCellularAccess
默認(rèn)設(shè)置為允許, 其他通通由用戶自定義.
至于 NSMutableURLRequest
和 NSURLSessionConfiguration
的部分屬性沖突, 這部分官方已經(jīng)在注釋里講的很明白了, 用戶可自行斟酌.
FKControl
這就是個(gè)控制任務(wù)的, 激活幕帆、暫停侮繁、繼續(xù)、停止如孝、刪除, 沒什么好說的, 獨(dú)立出來只是為了面向用戶, 不用跟其他私有模塊產(chǎn)生沖突, 而且多出一層, 可操作性也會多一層.
FKMiddleware
中間件模塊, 我也就在爬蟲框架和后端框架中見過, FKDownloader
中有這種模塊主要是為了有一種可以統(tǒng)一處理的方式.
目前該模塊只在生成 NSURLSessionDownloadTask
前, 下載成功/失敗后會有介入, 前者是為了諸如請求統(tǒng)一加簽宪哩、繞過瀏覽器限制等操作, 后者可以當(dāng)成 NSURLSession
代理中下載成功/失敗的回調(diào)即可.
更多的操作可根據(jù)業(yè)務(wù)自行調(diào)整.
再來是私有模塊.
FKEngine
該模塊基本就是框架運(yùn)轉(zhuǎn)的核心.
在一般下載邏輯中, 會為每個(gè)任務(wù)創(chuàng)建一個(gè)計(jì)時(shí)器, 實(shí)現(xiàn)進(jìn)度信息分發(fā)邏輯, 但這一塊兒其實(shí)用不了那么多, 還會因?yàn)楣芾聿贿^來出現(xiàn)問題(0.x版本就有這類問題), 所以在 FKEngine
中只有兩個(gè)個(gè)計(jì)時(shí)器, 只為了更簡潔的操作, 畢竟在實(shí)際業(yè)務(wù)中, 這些進(jìn)度條各走各還是一起走都無所謂,
計(jì)時(shí)器主要完成以下任務(wù):
- 檢查任務(wù)隊(duì)列, 進(jìn)行下一個(gè)任務(wù)
首先, FKBuilder
的預(yù)處理會將生成的任務(wù)信息模型(FKCacheRequestModel
) 存入 FKCache
的緩存隊(duì)列中, 但不開始任務(wù), 也不創(chuàng)建 NSURLSessionDownloadTask
, 這是前提, 與計(jì)時(shí)器無關(guān).
然后, 計(jì)時(shí)器被觸發(fā)后就會檢查隊(duì)列中正在執(zhí)行的任務(wù)是否超過設(shè)置, 超過了就什么也不做, 沒超過就開始用任務(wù)信息創(chuàng)建 NSURLSessionDownloadTask
, 進(jìn)行下載, 期間走過中間件流程, 本地信息緩存流程, 監(jiān)聽信息流程等等流程.
執(zhí)行任務(wù)計(jì)時(shí)器的間隔為 1s, 不可自定義.
- 分發(fā)任務(wù)信息
在使用 FKMessager
時(shí), 回調(diào)會被緩存, 計(jì)時(shí)器被觸發(fā)后將會輪詢執(zhí)行這些回調(diào)
以上任務(wù)都會在計(jì)時(shí)器觸發(fā)后執(zhí)行. 默認(rèn)情況下, 這個(gè)計(jì)時(shí)器是停止的, 需要用 FKConfigure
來激活, 這是為了保證在 NSURLSession
被創(chuàng)建后再執(zhí)行任務(wù).
計(jì)時(shí)器重復(fù)時(shí)間默認(rèn)設(shè)定為 1s, 這個(gè)時(shí)間剛剛好, 少了太頻繁, 多了感覺慢.
也支持用戶自定義速率, 目前 1 倍速率為 0.2 秒, 倍率在 1 ~ 10 倍區(qū)間可自定義.
除此之外, 應(yīng)用啟動后還會去查詢后臺已經(jīng)存在的下載任務(wù), 將這些任務(wù)添加到緩存中, 讓它們和其他任務(wù)一致.
基本上其他模塊只是制造信息, 輸出信息和保存信息, 而 FKEngine
則是讓整個(gè)框架活過來.
FKCache
主要負(fù)責(zé)信息緩存, 任務(wù)的信息, NSURLSessionDownloadTask
等等.
獨(dú)立的緩存模塊是必須的, 要緩存的信息有很多, 集中起來更容易管理.
在 FKDownloader
中, 對任務(wù)有一個(gè)主要理念: 任務(wù)即文件, 文件即任務(wù)
, 每一個(gè)任務(wù)都有一個(gè)屬于自己的唯一標(biāo)識, 標(biāo)識與用戶輸入的鏈接息息相關(guān), 也和本地緩存有著千絲萬縷的聯(lián)系.
在 FKDownloader
中的文件就是 FKCacheRequestModel
對應(yīng)的歸檔文件, 這個(gè)模型提供的信息有:
@property (nonatomic, strong) NSString *requestID; // 請求標(biāo)識, SHA256(URL)
@property (nonatomic, strong) NSString *requestSingleID; // 唯一請求標(biāo)識, SingleNumber_SHA256(URL)
@property (nonatomic, strong) NSString *idx; // 唯一順序編碼
@property (nonatomic, strong) NSString *url; // 原始請求鏈接
@property (nonatomic, strong) NSMutableURLRequest *request; // 請求
@property (nonatomic, assign) FKState state; // 請求狀態(tài)
@property (nonatomic, assign) int64_t receivedLength; // 接收的數(shù)據(jù)長度
@property (nonatomic, assign) int64_t dataLength; // 數(shù)據(jù)長度
@property (nonatomic, strong) NSString *extension; // 文件后綴, `.*`
@property (nonatomic, strong, nullable) NSData *resumeData; // 恢復(fù)數(shù)據(jù)
@property (nonatomic, strong, nullable) NSError *error; // 錯(cuò)誤
基本上可以構(gòu)成/恢復(fù)任務(wù)的信息都在里面, 每一個(gè)任務(wù)都有自己的文件夾保存這些信息, 分而治之有利于管理, 0.x版本中都是所有任務(wù)都在一個(gè)文件中, 不管從性能上看, 還是管理上看, 都有很大的問題.
FKObserver
眾多任務(wù)需要監(jiān)聽的流程太過繁雜也太過分散, 系統(tǒng) BUG 還導(dǎo)致這些監(jiān)聽還需要重新添加, 這就更分散了. 而獲取進(jìn)度信息在業(yè)務(wù)上來講并不頻繁使用, 這些監(jiān)聽到的信息全放在任務(wù)信息模型里也不合適, 那么, 直接獨(dú)立出來成模塊豈不美哉.
FKObserver
以專門監(jiān)聽 NSURLSessionDownloadTask
而生, 所有任務(wù)的進(jìn)度信息都在這里.
FKObserver
使用 FKObserverModel
保存進(jìn)度信息, 基本信息如下:
@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
配合完美, 一個(gè)對外, 一個(gè)對內(nèi).
FKScheduler
FKBuilder
和 FKEngine
之間的模塊, FKControl
的實(shí)現(xiàn), 主要任務(wù)如下:
- 對
FKBuilder
的預(yù)處理邏輯進(jìn)行了更細(xì)節(jié)的處理. 如創(chuàng)建任務(wù)信息文件, 添加內(nèi)存/本地緩存, 忽略已存在任務(wù)等. - 實(shí)現(xiàn)
FKControl
的操作, 激活、暫停第晰、繼續(xù)锁孟、取消、刪除.
FKSessionDelegater
實(shí)現(xiàn) NSURLSession
的代理, 沒啥好說, 單獨(dú)摘出來是因?yàn)榇矸椒ㄟ€是很多很復(fù)雜的, 為了之后更好的擴(kuò)展, 這樣更好一些.
還有一些輔助用模塊
FKSingleNumber
在執(zhí)行下一個(gè)任務(wù)時(shí), 哪個(gè)才是下一個(gè)? 按添加順序可不一定準(zhǔn), 所以 FKDownloader
直接使用 stdatomic.h
中的 atomic_ullong
來創(chuàng)建一個(gè)不受線程影響的原子數(shù), 再讓它被獲取時(shí)自增.
FKCacheRequestModel
的 requestSingleID
就是原子數(shù)和下載鏈接的哈希值拼接出來的.
當(dāng)然, 從業(yè)務(wù)上來講, 任務(wù)的執(zhí)行順序是否按照列表所示順序依次進(jìn)行好像并不怎么重要.
FKFileManager
文件管理的封裝, 主要負(fù)責(zé)創(chuàng)建/刪除任務(wù)對應(yīng)的文件/文件夾.
FKLogger
輔助信息日志, 這倒是沒啥好說的, 只是為了更方便調(diào)試, 信息只會在 DEBUG 環(huán)境下執(zhí)行.
FKCoder
URL 編解碼.
先說編碼, 有一個(gè)問題便是用戶傳入的下載鏈接是否已經(jīng)編碼過, 這個(gè)可以循環(huán)解碼至和上一個(gè)結(jié)果一致時(shí)停下, 這個(gè)問題不算太大. 但 URL 可能有帶有 emoji
,fragment
的情況, emoji
可以用系統(tǒng)的 URLQueryAllowedCharacterSet
直接處理, 但 fragment
就會編碼錯(cuò)誤, 這時(shí)就需要分段處理.
再說解碼, 這個(gè)就簡單了, 直接 stringByRemovingPercentEncoding
走起.
FKMIMEType.
既然是文件下載, 那基本上都有后綴吧, 直接從 URL 里獲取是不現(xiàn)實(shí)的, 畢竟有的鏈接是加簽的, 后綴是不存在的, 所以要用 Response
中的 Content-Type
也就是 MIMEType
來轉(zhuǎn)為后綴名.
系統(tǒng)可以講 MIMEType
轉(zhuǎn)為后綴, 但并不全面, 所以需要將其他常用的加入轉(zhuǎn)換列表中, 如果實(shí)在沒有對應(yīng)后綴名, 就用 unknown
為后綴名.
關(guān)于后臺下載的 Tips
后臺下載功能中也存在一些需要知道的東西.
后臺任務(wù)由系統(tǒng)啟動后的各種限制
先說結(jié)論, 目前沒有什么好的方法去繞過. 系統(tǒng)的限制基本上有以下幾種:
- 限制下載速度
- 限制何時(shí)啟動下一個(gè)任務(wù)的時(shí)間
- 限制任務(wù)啟動數(shù)量
- ....
總的來說, iOS 為了達(dá)到完美的運(yùn)行并且不會影響系統(tǒng)的穩(wěn)定性, 后臺下載的內(nèi)核做了非常多的限制, 而且為什么有這些限制, 又是怎樣做到的, 官方并沒有明說, 只能從這里看出一個(gè)重點(diǎn)信息, NSURLSession Background Download 是系統(tǒng)包攬的, 開發(fā)者最好不要深入研究.
測試后臺下載流程
從這里可以看出, 測試時(shí)一定要嚴(yán)格遵守以下幾點(diǎn):
- Test on a real device, not the simulator. 在真機(jī)上測試, 而不是模擬器.
- Run your app from the Home screen rather than running it from Xcode. 從主屏幕上運(yùn)行, 而不是 Xcode 直接運(yùn)行.
- Do not use force quit to test the ‘relaunch in the background case’. 不要從任務(wù)管理中強(qiáng)制退出 App 來模擬后臺下載流程中的強(qiáng)制中斷 App 邏輯, 而是在合適的地方使用 exit() 來退出 App.