有始有终,设计一个结构合理的下载模块
完成開(kāi)發(fā)任務(wù)的同時(shí),我們總希望自己能夠交付高質(zhì)量的代碼。代碼質(zhì)量的測(cè)度有很多方法,可擴(kuò)展性、可復(fù)用性是其中的兩項(xiàng)指標(biāo)。設(shè)計(jì)模式的理論能夠非常有效地指導(dǎo)代碼設(shè)計(jì),但是光談這些理論是非常抽象的,本文針對(duì)下載這個(gè)場(chǎng)景,結(jié)合設(shè)計(jì)模式的一些理論,談一談如何設(shè)計(jì)一個(gè)結(jié)構(gòu)較為合理的下載模塊。
Step.1 別急,先進(jìn)行”需求評(píng)審“
在著手編碼之前,先明確功能需求、技術(shù)需求,然后進(jìn)行初步的思考。
從目標(biāo)出發(fā)
從目標(biāo)出發(fā),能夠幫助明確設(shè)計(jì)過(guò)程中的側(cè)重點(diǎn)。對(duì)于下載這個(gè)場(chǎng)景,很直觀可以想到,它涉及到的文件操作、持久化存儲(chǔ)等步驟是會(huì)頻繁出現(xiàn)在一個(gè)項(xiàng)目中的。所以我會(huì)希望為下載模塊寫(xiě)的大量代碼能夠被良好復(fù)用。同時(shí)可以預(yù)見(jiàn),下載這一場(chǎng)景是非常容易出現(xiàn)后續(xù)需求變更或者增加的,沒(méi)準(zhǔn)今天只下載視頻,明天又需要添加對(duì)音頻、對(duì) zip 文件的支持;對(duì)于數(shù)據(jù)庫(kù)存儲(chǔ)框架,可能目前在使用 FMDB,后續(xù)又要更換為 WCDB。所以,也對(duì)這個(gè)模塊的可擴(kuò)展性、易修改性提出了要求。
結(jié)合一點(diǎn)點(diǎn)理論
設(shè)計(jì)模式中有幾大原則,剛開(kāi)始接觸我們總感到難以把握。因?yàn)樗鼈兒?jiǎn)短得像幾字真言,而實(shí)際的場(chǎng)景卻有千千萬(wàn)萬(wàn)種。那么,就從最易理解的**“單一職責(zé)原則”開(kāi)始。簡(jiǎn)單來(lái)說(shuō),一個(gè)單獨(dú)的模塊應(yīng)該只負(fù)責(zé)一個(gè)單獨(dú)的任務(wù),任務(wù)的粒度越細(xì),它和其他模塊的耦合性越低,它也越容易被復(fù)用。而遵循“依賴倒置原則”,則會(huì)有效提高代碼的易修改性。比如對(duì)于數(shù)據(jù)庫(kù)模塊,在實(shí)際使用某一數(shù)據(jù)庫(kù)框架進(jìn)行存取操作的實(shí)現(xiàn)類(lèi)之上,再抽象出一層接口類(lèi)。在下載過(guò)程中只使用接口類(lèi)中提供的方法,而接口類(lèi)中方法的具體實(shí)現(xiàn),則由下層的實(shí)現(xiàn)類(lèi)完成。這樣,當(dāng)我們把數(shù)據(jù)庫(kù)框架由 FMDB 替換為 WCDB 時(shí),只需對(duì)實(shí)現(xiàn)類(lèi)的代碼進(jìn)行修改,修改的目標(biāo)則是使用新框架再次實(shí)現(xiàn)接口類(lèi)中聲明的方法,這也就是所謂的“針對(duì)接口編程”,而非”針對(duì)實(shí)現(xiàn)編程“。它帶來(lái)的好處是顯而易見(jiàn)的:在數(shù)據(jù)庫(kù)框架的替換過(guò)程中,最上層的業(yè)務(wù)代碼完全無(wú)需改動(dòng)**,只需對(duì)數(shù)據(jù)庫(kù)操作的實(shí)現(xiàn)類(lèi)進(jìn)行修改即可。
模塊化的目的
有一件事是需要明確的,我們常談的“模塊化”,并非對(duì)所有模塊都追求任意場(chǎng)景下的可復(fù)用。因?yàn)槟K會(huì)分為業(yè)務(wù)模塊和通用模塊,通用模塊力求做到任意場(chǎng)景下的可復(fù)用,而業(yè)務(wù)模塊則專(zhuān)注于完成某一需求場(chǎng)景。雖然“下載”這個(gè)詞在很多項(xiàng)目中會(huì)出現(xiàn),但不同的項(xiàng)目中對(duì)它的定義是不同的。有的“下載”僅僅意指下載單個(gè)的文件,而有的下載則指的是某一場(chǎng)景下所有內(nèi)容的本地緩存。
在這篇文章中,我預(yù)設(shè)的場(chǎng)景是一個(gè)下載任務(wù)中會(huì)包括各種具體的子任務(wù),舉個(gè)例子,一個(gè)下載任務(wù)可能由三個(gè)視頻文件、兩個(gè)音頻文件、三張圖片、兩個(gè)網(wǎng)絡(luò)請(qǐng)求的 JSON 格式結(jié)果組成。
因此,我會(huì)把本文所說(shuō)的“下載”歸入業(yè)務(wù)模塊,它不追求做到任意場(chǎng)景下的可復(fù)用,但它能夠很好地完成這個(gè)較復(fù)雜場(chǎng)景下的下載任務(wù)。而這個(gè)業(yè)務(wù)模塊中所包含的文件下載、圖片緩存、文件操作等具體步驟,其實(shí)是無(wú)關(guān)業(yè)務(wù)的,那么它們便可以歸為通用模塊。在其他進(jìn)行圖片緩存的場(chǎng)景下,可以使用這里的圖片緩存模塊,而其他的文件操作場(chǎng)景,也可以使用這里的文件操作模塊。它們的具體分析會(huì)在下文展開(kāi)。
Step.2 給出設(shè)計(jì)方案
結(jié)合文章第一部分的分析,著手進(jìn)行方案的設(shè)計(jì)。
“下載”不是單單一件事
通常意義上的下載,是指將云端的資源獲取到本地磁盤(pán)的過(guò)程。對(duì)于 iOS 應(yīng)用,下載的目的多是進(jìn)行某些內(nèi)容的離線展示。一個(gè)完整的下載過(guò)程,應(yīng)該由以下的步驟組成:
-
文件操作
對(duì)于所下載的文件,需要確定它在本地的存儲(chǔ)路徑;給定某個(gè) key 值,需要獲取對(duì)應(yīng)文件的存儲(chǔ)路徑;對(duì)于某個(gè)指定的路徑,會(huì)有檢查文件存在性、完整性等操作;下載過(guò)程中不斷進(jìn)行文件寫(xiě)入,刪除已下載內(nèi)容時(shí)涉及文件刪除、目錄刪除;除此之外,還有獲取各個(gè)系統(tǒng)目錄、獲取磁盤(pán)空間數(shù)據(jù)等常規(guī)操作。若涉及安全性需求,還會(huì)有文件加密、解密操作。因此,將文件操作封裝為一個(gè)單獨(dú)模塊是一個(gè)明智的選擇。文件操作不僅僅會(huì)在下載這個(gè)場(chǎng)景中出現(xiàn),因此,在這個(gè)模塊的實(shí)現(xiàn)過(guò)程中應(yīng)該盡量剝離業(yè)務(wù)相關(guān)的內(nèi)容,力求成為一個(gè)通用的工具模塊。
-
數(shù)據(jù)庫(kù)操作
基于文章第一部分中給出的場(chǎng)景,這里的下載任務(wù)應(yīng)該是結(jié)構(gòu)化的數(shù)據(jù)。無(wú)論網(wǎng)絡(luò)狀況是否正常,已下載的內(nèi)容都能夠正常展示,所以下載記錄應(yīng)該被持久化存儲(chǔ)?;谝陨蟽牲c(diǎn),數(shù)據(jù)庫(kù)的使用是自然的選擇。應(yīng)該明確的是,數(shù)據(jù)庫(kù)存儲(chǔ)的是下載任務(wù)記錄,或叫做日志,而非下載的文件??紤]到 iOS 中數(shù)據(jù)庫(kù)框架的多樣性和業(yè)務(wù)方對(duì)數(shù)據(jù)庫(kù)性能的持續(xù)追求,很容易預(yù)見(jiàn)到數(shù)據(jù)庫(kù)框架在未來(lái)的替換工作。因此對(duì)于這個(gè)模塊,上文也進(jìn)行了分析,那就是依照依賴倒置原則,分成抽象的接口類(lèi)和具體的實(shí)現(xiàn)類(lèi)。
-
較大體積文件的下載
在下載的需求中,視頻、音頻、zip 文件等體積較大的文件是很常見(jiàn)的。因此一個(gè)只針對(duì)較大體積文件的下載模塊模塊必不可少。它不涉及任何具體的業(yè)務(wù)細(xì)節(jié),它的任務(wù)僅僅是根據(jù)給定的文件 url 和本地存儲(chǔ)的路徑,完成該文件的下載。做到這個(gè)模塊的高內(nèi)聚是比較容易的,因此強(qiáng)烈建議將這部分封裝為一個(gè)通用模塊,以滿足任何場(chǎng)景下的文件下載需求。為減少通用模塊之間的橫向依賴,一個(gè)思路是本地路徑由上層的業(yè)務(wù)模塊調(diào)用文件操作模塊獲得,然后傳遞給本模塊,而非本模塊直接調(diào)用文件操作模塊;對(duì)于文件寫(xiě)入操作,可直接使用系統(tǒng)的 NSFileManager。同時(shí)也有另一種思路,大文件下載和文件操作之間的依賴是自然、可接受的,允許下載模塊依賴文件操作模塊。這些沒(méi)有標(biāo)準(zhǔn)答案,可以自行取舍。
-
圖片的下載
有時(shí)候下載任務(wù)中會(huì)包含圖片下載,按照體積來(lái)看,將圖片下載歸入文件類(lèi)型也不為過(guò)。但是圖片的緩存在iOS的開(kāi)發(fā)中是一個(gè)積淀已深的話題,我們擁有 YYWebImage、SDWebImage 等優(yōu)秀的圖片緩存框架,有什么理由再去重復(fù)造一個(gè)性能未必更優(yōu)的輪子呢?除此之外,剛剛提到的兩個(gè)圖片框架基本應(yīng)用在了絕大多數(shù)的iOS網(wǎng)絡(luò)應(yīng)用中,所以很有可能出現(xiàn)的場(chǎng)景是:已經(jīng)下載過(guò)的圖片,在項(xiàng)目中的某處不相關(guān)的地方用上述圖片框架進(jìn)行加載。如果圖片下載使用這些框架的緩存器來(lái)實(shí)現(xiàn),那么在上述場(chǎng)景下,**框架會(huì)從本地緩存中尋找到目標(biāo)圖片,避免重復(fù)的云端下載,達(dá)到了有效且明顯的優(yōu)化效果?;诰植啃栽?#xff0c;這種情景的命中率還是不可忽略的。**因此,建議將圖片的下載拆分為一個(gè)內(nèi)部實(shí)現(xiàn)使用上述框架的圖片緩存器。
-
網(wǎng)絡(luò)請(qǐng)求結(jié)果的緩存
有的下載場(chǎng)景中,需要對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行緩存。網(wǎng)絡(luò)請(qǐng)求的結(jié)果多為 JSON 格式的數(shù)據(jù),體積較小,屬于輕量的下載內(nèi)容。我的實(shí)現(xiàn)是網(wǎng)絡(luò)請(qǐng)求緩存和圖片緩存作為 cache 模塊的一部分,整體封裝一個(gè) cache 模塊。也可以將這兩者分開(kāi)模塊化,視具體業(yè)務(wù)需求靈活決定。
-
特定場(chǎng)景下載的業(yè)務(wù)模塊
以上列出的模塊,基本都可以向可廣泛復(fù)用的通用模塊努力。上文提到,模塊化中,也包括專(zhuān)注具體場(chǎng)景的業(yè)務(wù)模塊。在本文的業(yè)務(wù)場(chǎng)景下,我封裝了一個(gè)業(yè)務(wù)模塊。它的職責(zé)是:持久化維護(hù)已下載和正在下載任務(wù)的list;根據(jù)按固定格式提交的下載任務(wù),解析出結(jié)構(gòu)化的任務(wù)結(jié)構(gòu);對(duì)于不同類(lèi)型的子任務(wù),使用上述對(duì)應(yīng)的通用模塊完成下載;同時(shí)負(fù)責(zé)協(xié)調(diào)各子任務(wù)之間的同步關(guān)系;在所有子任務(wù)完成下載后,檢查整個(gè)結(jié)構(gòu)的文件完整性;通過(guò)完整性校驗(yàn)后,進(jìn)行數(shù)據(jù)庫(kù)存儲(chǔ)操作,存儲(chǔ)該次下載日志;在整個(gè)活動(dòng)周期內(nèi),模塊還負(fù)責(zé)下載任務(wù)狀態(tài)的更新。
模塊整體結(jié)構(gòu)
通過(guò)對(duì)整個(gè)下載過(guò)程的分析,我們拆分出了幾個(gè)模塊。依照單一職責(zé)原則,將每個(gè)模塊的職責(zé)劃分到了較為合適的粒度,都能夠做到一定程度上的復(fù)用。對(duì)于其中擴(kuò)展可能較高的模塊,依照依賴倒置原則,抽象出了一層接口類(lèi),避免了未來(lái)底層修改時(shí)對(duì)上層業(yè)務(wù)代碼的影響。在模塊化的應(yīng)用上,也做到了目的明確、合理拆分。
下圖即是整體的示意圖:
Step.3 完成具體實(shí)現(xiàn)吧!
其實(shí)寫(xiě)完第二部分,本文的寫(xiě)作目的已經(jīng)差不多達(dá)到。大家從標(biāo)題可以感受到,本文側(cè)重點(diǎn)在于對(duì)”下載“這個(gè)場(chǎng)景運(yùn)用一些理論的指導(dǎo)進(jìn)行較為合理的代碼結(jié)構(gòu)設(shè)計(jì)。不過(guò)為做到有始有終——“從理論分析開(kāi)始,用具體實(shí)現(xiàn)來(lái)結(jié)尾”,這部分對(duì)實(shí)現(xiàn)細(xì)節(jié)進(jìn)行一些討論,提供一些“干貨”,這些方案面對(duì)不同場(chǎng)景會(huì)有不同的優(yōu)劣表現(xiàn),僅供參考。
-
文件操作模塊
這部分我的實(shí)現(xiàn)是使用系統(tǒng)的 NSFileManager 進(jìn)行文件存在性判斷等基本操作。對(duì)于本地存儲(chǔ)的目標(biāo)路徑,生成規(guī)則為文件 URL 做 md5 操作,再添加具體的文件類(lèi)型后綴。在安全性較高的場(chǎng)景中,所下載的文件都來(lái)自自有的服務(wù)器,那么文件正確性校驗(yàn)可以由后端提供部分支持,如對(duì)于每個(gè)文件都返回特定的校驗(yàn)值,在本地下載完成后,使用由已下載文件生成的校驗(yàn)值和后端提供的進(jìn)行比對(duì)。
-
數(shù)據(jù)庫(kù)模塊
對(duì)于數(shù)據(jù)庫(kù)中需要存儲(chǔ)什么字段,我的意見(jiàn)是這樣的:對(duì)于某個(gè)具體的文件,存儲(chǔ)初始 url、文件在本地存儲(chǔ)的路徑、文件大小、更新時(shí)間等基本信息。對(duì)于結(jié)構(gòu)化的整條下載記錄,則將還原初始下載任務(wù)的所需字段都進(jìn)行存儲(chǔ)。具體解釋下,初始下載任務(wù)的提交時(shí)多是使用業(yè)務(wù)方的數(shù)據(jù)類(lèi)型,比如一篇微博展示時(shí)的 model ,一篇文章展示時(shí)的 model。而下載任務(wù)提交到下載模塊后,我們會(huì)將初始的數(shù)據(jù)類(lèi)型轉(zhuǎn)化為下載模塊的規(guī)定的數(shù)據(jù)格式。若涉及到斷點(diǎn)續(xù)傳等場(chǎng)景,便會(huì)存在 app 重啟后,由從數(shù)據(jù)庫(kù)中取得的下載模塊所用數(shù)據(jù)格式向初始業(yè)務(wù)方數(shù)據(jù)格式的逆轉(zhuǎn)化,這時(shí)就需要初始任務(wù)所有必要的狀態(tài)信息,從而進(jìn)行現(xiàn)場(chǎng)恢復(fù),繼續(xù)進(jìn)行下載。
上文說(shuō)到,下載管理業(yè)務(wù)模塊需要維護(hù)下載中、已下載任務(wù)的 list,用什么來(lái)區(qū)分狀態(tài)呢?我的實(shí)現(xiàn)是為下載記錄添加標(biāo)識(shí)是否完成的字段,這樣當(dāng) app 重啟后,從數(shù)據(jù)庫(kù)中取得所有的下載記錄,若某條記錄被標(biāo)識(shí)為未完成,那么它便是需要還原為初始下載任務(wù)的記錄,被歸入下載中 list。
-
大體積文件下載模塊
關(guān)于這部分的討論已經(jīng)有很多,本文不再贅述。值得一提的是,這個(gè)通用組件依然會(huì)面臨底層實(shí)現(xiàn)更換或者版本升級(jí)的問(wèn)題,所以依照依賴倒置抽象出接口層的思路在這里依然適用。
-
緩存模塊
關(guān)于圖片的緩存在上文已經(jīng)詳細(xì)討論。對(duì)于 JSON 格式的網(wǎng)絡(luò)請(qǐng)求結(jié)果,iOS 中一般使用 NSDictionary 存儲(chǔ),它支持 NSCoding 協(xié)議,因此 YYCache、EGOCache等緩存框架都是可以使用的。這部分的接口設(shè)計(jì)比較直白,為指定 key 對(duì)應(yīng)的值進(jìn)行緩存,根據(jù)給定 key 返回對(duì)應(yīng)的緩存值,以及移除給定 key 對(duì)應(yīng)的內(nèi)容。抽象接口層的思路,照例適用。
-
下載管理業(yè)務(wù)模塊
在項(xiàng)目的很多地方可能都需要獲知當(dāng)前下載模塊的狀態(tài),所以這里使用單例實(shí)現(xiàn)是一個(gè)比較好的選擇。在整個(gè)下載過(guò)程的最初,它根據(jù)提交的每一個(gè)初始任務(wù)數(shù)據(jù),解析出具體的子任務(wù)類(lèi)型,調(diào)用對(duì)應(yīng)的子模塊完成子任務(wù)的下載。同一下載任務(wù)下的各子任務(wù)之間應(yīng)該是異步的,所以 dispatch group 是一個(gè)直觀的選擇。順序提交的所有初始任務(wù)之間,則是同步的關(guān)系,這里可以使用類(lèi)似隊(duì)列的結(jié)構(gòu)來(lái)管理。下面給出一個(gè)示意圖:
對(duì)于下載中、已下載這兩種狀態(tài)的區(qū)分,這里提供一個(gè)改進(jìn)思路:在某個(gè)初始任務(wù)真正開(kāi)始下載之前,就向數(shù)據(jù)庫(kù)中插入一條新的下載記錄,設(shè)置狀態(tài)字段為未完成,當(dāng)所有子任務(wù)均完成且通過(guò)完整性校驗(yàn)后,更新?tīng)顟B(tài)字段為完成。
最后,為大家提供一個(gè)業(yè)務(wù)模塊的樣例偽代碼,用以展示整個(gè)下載流程。
//下載管理業(yè)務(wù)模塊的接口列表(大意展示)//業(yè)務(wù)方的model @class OriginModel;@interface DownloadManager : NSObject //獲取下載管理對(duì)象(單例) + (instancetype)sharedInstance; //獲取下載中的任務(wù) - (NSArray<OriginModel *> *)getDownloadingItems; //獲取已下載的任務(wù) - (NSArray<OriginModel*> *)getDownloadedItems; //根據(jù)id獲取已下載的item - (OriginModel *)getDownloadedItemById:(id<NSCopying>)itemId; //是否下載過(guò)指定id的item - (BOOL)didDownloadedItem:(id<NSCopying>)itemId; //批量下載 - (void)downloadItems:(NSArray<OriginModel*> *)items; //暫停下載 - (void)pauseDownloadForItem:(id<NSCopying>)itemId; //恢復(fù)下載 - (void)resumeDownloadForItem:(id<NSCopying>)itemId; //取消下載 - (void)cancelDownloadForItem:(id<NSCopying>)itemId; @end 復(fù)制代碼//下載管理業(yè)務(wù)模塊的主要實(shí)現(xiàn)@implementation DownloadManager- (void)downloadItems:(NSArray<OriginModel *> *)items {// 解析任務(wù)結(jié)構(gòu),將所有任務(wù)push進(jìn)任務(wù)隊(duì)列MissionStruct *oneStruct = [self analyzeMission];for (MissionItem *item in oneStruct) {[self.missionList pushItem:item];}... // 若非空,從任務(wù)隊(duì)列中取出任務(wù)元素if (![self.missionList isEmpty]) {MissionItem *oneMission = [self.missionList pop];[self handleMission:oneMission];} }- (void)handleMission:(MissionItem *)mission {// 調(diào)用數(shù)據(jù)庫(kù)模塊,插入一條新紀(jì)錄[DatabaseManager insertMission:mission];dispatch_group_t downloadGroup;// 下載視頻for (videoMission in mission.videos) {dispatch_group_enter(downloadGroup);// 調(diào)用文件管理模塊,獲取該url對(duì)應(yīng)的文件路徑targetPath = [FileManager pathForURL:videoMission.url];// 調(diào)用大文件下載模塊,下載該視頻[FileDownloadManager downloadFile:videoMission.urltargetPath:targetPathsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 下載音頻for (audioMission in mission.audios) {dispatch_group_enter(downloadGroup);// 調(diào)用文件管理模塊,獲取該url對(duì)應(yīng)的文件路徑targetPath = [FileManager pathForURL:audioMission.url];// 調(diào)用大文件下載模塊,下載該音頻[FileDownloadManager downloadFile:audioMission.urltargetPath:targetPathsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 緩存圖片for (imageMission in mission.images) {dispatch_group_enter(downloadGroup);// 調(diào)用圖片緩存模塊,緩存該圖片[ImageCacheManager cacheImage:imageMission.urlsuccess:^(){dispatch_group_leave(downloadGroup);}];}// 緩存網(wǎng)絡(luò)請(qǐng)求for (contentMission in mission.contents) {dispatch_group_enter(downloadGroup);// 調(diào)用網(wǎng)絡(luò)請(qǐng)求緩存模塊,緩存該網(wǎng)絡(luò)請(qǐng)求[RequestCacheManager cacheRequest:contentMission.urlsuccess:^(){dispatch_group_leave(downloadGroup);}];}...// 所有子任務(wù)均完成dispatch_group_notify(downloadGroup, dispatch_get_global_queue(0, 0), ^{// 通過(guò)完整性校驗(yàn)if ([self verifyAllSubMission:mission]) {// 調(diào)用數(shù)據(jù)庫(kù)模塊,更新該下載紀(jì)錄[DatabaseManager updateMission:mission];} else {// 未通過(guò)完整性校驗(yàn),移除數(shù)據(jù)庫(kù)對(duì)應(yīng)記錄[DatabaseManager removeMission:mission];}}); }@end 復(fù)制代碼知識(shí)小集是一個(gè)團(tuán)隊(duì)公眾號(hào),主要定位在移動(dòng)開(kāi)發(fā)領(lǐng)域,分享移動(dòng)開(kāi)發(fā)技術(shù),包括 iOS、Android、小程序、移動(dòng)前端、React Native、weex 等。每周都會(huì)有 原創(chuàng) 文章分享,我們的文章都會(huì)在公眾號(hào)首發(fā)。歡迎關(guān)注查看更多內(nèi)容。
總結(jié)
以上是生活随笔為你收集整理的有始有终,设计一个结构合理的下载模块的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Elasticsearch 参考指南(重
- 下一篇: 视频版权保护