善始善終,設計一個結構合理的下載模塊

完成開發任務的同時,咱們總但願本身可以交付高質量的代碼。代碼質量的測度有不少方法,可擴展性、可複用性是其中的兩項指標。設計模式的理論可以很是有效地指導代碼設計,可是光談這些理論是很是抽象的,本文針對下載這個場景,結合設計模式的一些理論,談一談如何設計一個結構較爲合理的下載模塊。前端

Step.1 別急,先進行」需求評審「

在着手編碼以前,先明確功能需求、技術需求,而後進行初步的思考。ios

從目標出發

從目標出發,可以幫助明確設計過程當中的側重點。對於下載這個場景,很直觀能夠想到,它涉及到的文件操做、持久化存儲等步驟是會頻繁出如今一個項目中的。因此我會但願爲下載模塊寫的大量代碼可以被良好複用。同時能夠預見,下載這一場景是很是容易出現後續需求變動或者增長的,沒準今天只下載視頻,明天又須要添加對音頻、對 zip 文件的支持;對於數據庫存儲框架,可能目前在使用 FMDB,後續又要更換爲 WCDB。因此,也對這個模塊的可擴展性、易修改性提出了要求。數據庫

結合一點點理論

設計模式中有幾大原則,剛開始接觸咱們總感到難以把握。由於它們簡短得像幾字真言,而實際的場景卻有千千萬萬種。那麼,就從最易理解的**「單一職責原則」開始。簡單來講,一個單獨的模塊應該只負責一個單獨的任務,任務的粒度越細,它和其餘模塊的耦合性越低,它也越容易被複用。而遵循「依賴倒置原則」,則會有效提升代碼的易修改性。好比對於數據庫模塊,在實際使用某一數據庫框架進行存取操做的實現類之上,再抽象出一層接口類。在下載過程當中只使用接口類中提供的方法,而接口類中方法的具體實現,則由下層的實現類完成。這樣,當咱們把數據庫框架由 FMDB 替換爲 WCDB 時,只需對實現類的代碼進行修改,修改的目標則是使用新框架再次實現接口類中聲明的方法,這也就是所謂的「針對接口編程」,而非」針對實現編程「。它帶來的好處是顯而易見的:在數據庫框架的替換過程當中,最上層的業務代碼徹底無需改動**,只需對數據庫操做的實現類進行修改便可。編程

模塊化的目的

有一件事是須要明確的,咱們常談的「模塊化」,並不是對全部模塊都追求任意場景下的可複用。由於模塊會分爲業務模塊和通用模塊,通用模塊力求作到任意場景下的可複用,而業務模塊則專一於完成某一需求場景。雖然「下載」這個詞在不少項目中會出現,但不一樣的項目中對它的定義是不一樣的。有的「下載」僅僅意指下載單個的文件,而有的下載則指的是某一場景下全部內容的本地緩存。小程序

在這篇文章中,我預設的場景是一個下載任務中會包括各類具體的子任務,舉個例子,一個下載任務可能由三個視頻文件、兩個音頻文件、三張圖片、兩個網絡請求的 JSON 格式結果組成後端

所以,我會把本文所說的「下載」納入業務模塊,它不追求作到任意場景下的可複用,但它可以很好地完成這個較複雜場景下的下載任務。而這個業務模塊中所包含的文件下載、圖片緩存、文件操做等具體步驟,實際上是無關業務的,那麼它們即可以歸爲通用模塊。在其餘進行圖片緩存的場景下,可使用這裏的圖片緩存模塊,而其餘的文件操做場景,也可使用這裏的文件操做模塊。它們的具體分析會在下文展開。設計模式

Step.2 給出設計方案

結合文章第一部分的分析,着手進行方案的設計。緩存

「下載」不是單單一件事

一般意義上的下載,是指將雲端的資源獲取到本地磁盤的過程。對於 iOS 應用,下載的目的可能是進行某些內容的離線展現。一個完整的下載過程,應該由如下的步驟組成:安全

  • 文件操做

對於所下載的文件,須要肯定它在本地的存儲路徑;給定某個 key 值,須要獲取對應文件的存儲路徑;對於某個指定的路徑,會有檢查文件存在性、完整性等操做;下載過程當中不斷進行文件寫入,刪除已下載內容時涉及文件刪除、目錄刪除;除此以外,還有獲取各個系統目錄、獲取磁盤空間數據等常規操做。若涉及安全性需求,還會有文件加密、解密操做。所以,將文件操做封裝爲一個單獨模塊是一個明智的選擇。文件操做不只僅會在下載這個場景中出現,所以,在這個模塊的實現過程當中應該儘可能剝離業務相關的內容,力求成爲一個通用的工具模塊。服務器

  • 數據庫操做

基於文章第一部分中給出的場景,這裏的下載任務應該是結構化的數據。不管網絡情況是否正常,已下載的內容都可以正常展現,因此下載記錄應該被持久化存儲。基於以上兩點,數據庫的使用是天然的選擇。應該明確的是,數據庫存儲的是下載任務記錄,或叫作日誌,而非下載的文件。考慮到 iOS 中數據庫框架的多樣性和業務方對數據庫性能的持續追求,很容易預見到數據庫框架在將來的替換工做。所以對於這個模塊,上文也進行了分析,那就是依照依賴倒置原則,分紅抽象的接口類和具體的實現類。

  • 較大致積文件的下載

在下載的需求中,視頻、音頻、zip 文件等體積較大的文件是很常見的。所以一個只針對較大致積文件的下載模塊模塊必不可少。它不涉及任何具體的業務細節,它的任務僅僅是根據給定的文件 url 和本地存儲的路徑,完成該文件的下載。作到這個模塊的高內聚是比較容易的,所以強烈建議將這部分封裝爲一個通用模塊,以知足任何場景下的文件下載需求。爲減小通用模塊之間的橫向依賴,一個思路是本地路徑由上層的業務模塊調用文件操做模塊得到,而後傳遞給本模塊,而非本模塊直接調用文件操做模塊;對於文件寫入操做,可直接使用系統的 NSFileManager。同時也有另外一種思路,大文件下載和文件操做之間的依賴是天然、可接受的,容許下載模塊依賴文件操做模塊。這些沒有標準答案,能夠自行取捨。

  • 圖片的下載

有時候下載任務中會包含圖片下載,按照體積來看,將圖片下載納入文件類型也不爲過。可是圖片的緩存在iOS的開發中是一個積澱已深的話題,咱們擁有 YYWebImageSDWebImage 等優秀的圖片緩存框架,有什麼理由再去重複造一個性能未必更優的輪子呢?除此以外,剛剛提到的兩個圖片框架基本應用在了絕大多數的iOS網絡應用中,因此頗有可能出現的場景是:已經下載過的圖片,在項目中的某處不相關的地方用上述圖片框架進行加載。若是圖片下載使用這些框架的緩存器來實現,那麼在上述場景下,**框架會從本地緩存中尋找到目標圖片,避免重複的雲端下載,達到了有效且明顯的優化效果。基於局部性原理,這種情景的命中率仍是不可忽略的。**所以,建議將圖片的下載拆分爲一個內部實現使用上述框架的圖片緩存器。

  • 網絡請求結果的緩存

有的下載場景中,須要對網絡請求進行緩存。網絡請求的結果多爲 JSON 格式的數據,體積較小,屬於輕量的下載內容。個人實現是網絡請求緩存和圖片緩存做爲 cache 模塊的一部分,總體封裝一個 cache 模塊。也能夠將這二者分開模塊化,視具體業務需求靈活決定。

  • 特定場景下載的業務模塊

以上列出的模塊,基本均可以向可普遍複用的通用模塊努力。上文提到,模塊化中,也包括專一具體場景的業務模塊。在本文的業務場景下,我封裝了一個業務模塊。它的職責是:持久化維護已下載和正在下載任務的list;根據按固定格式提交的下載任務,解析出結構化的任務結構;對於不一樣類型的子任務,使用上述對應的通用模塊完成下載;同時負責協調各子任務之間的同步關係;在全部子任務完成下載後,檢查整個結構的文件完整性;經過完整性校驗後,進行數據庫存儲操做,存儲該次下載日誌;在整個活動週期內,模塊還負責下載任務狀態的更新。

模塊總體結構

經過對整個下載過程的分析,咱們拆分出了幾個模塊。依照單一職責原則,將每一個模塊的職責劃分到了較爲合適的粒度,都可以作到必定程度上的複用。對於其中擴展可能較高的模塊,依照依賴倒置原則,抽象出了一層接口類,避免了將來底層修改時對上層業務代碼的影響。在模塊化的應用上,也作到了目的明確、合理拆分。

下圖便是總體的示意圖:

Step.3 完成具體實現吧!

其實寫完第二部分,本文的寫做目的已經差很少達到。你們從標題能夠感覺到,本文側重點在於對」下載「這個場景運用一些理論的指導進行較爲合理的代碼結構設計。不過爲作到善始善終——「從理論分析開始,用具體實現來結尾」,這部分對實現細節進行一些討論,提供一些「乾貨」,這些方案面對不一樣場景會有不一樣的優劣表現,僅供參考。

  • 文件操做模塊

這部分個人實現是使用系統的 NSFileManager 進行文件存在性判斷等基本操做。對於本地存儲的目標路徑,生成規則爲文件 URL 作 md5 操做,再添加具體的文件類型後綴。在安全性較高的場景中,所下載的文件都來自自有的服務器,那麼文件正確性校驗能夠由後端提供部分支持,如對於每一個文件都返回特定的校驗值,在本地下載完成後,使用由已下載文件生成的校驗值和後端提供的進行比對。

  • 數據庫模塊

對於數據庫中須要存儲什麼字段,個人意見是這樣的:對於某個具體的文件,存儲初始 url、文件在本地存儲的路徑、文件大小、更新時間等基本信息。對於結構化的整條下載記錄,則將還原初始下載任務的所需字段都進行存儲。具體解釋下,初始下載任務的提交時可能是使用業務方的數據類型,好比一篇微博展現時的 model ,一篇文章展現時的 model。而下載任務提交到下載模塊後,咱們會將初始的數據類型轉化爲下載模塊的規定的數據格式。若涉及到斷點續傳等場景,便會存在 app 重啓後,由從數據庫中取得的下載模塊所用數據格式向初始業務方數據格式的逆轉化,這時就須要初始任務全部必要的狀態信息,從而進行現場恢復,繼續進行下載。

上文說到,下載管理業務模塊須要維護下載中、已下載任務的 list,用什麼來區分狀態呢?個人實現是爲下載記錄添加標識是否完成的字段,這樣當 app 重啓後,從數據庫中取得全部的下載記錄,若某條記錄被標識爲未完成,那麼它即是須要還原爲初始下載任務的記錄,被納入下載中 list。

  • 大致積文件下載模塊

關於這部分的討論已經有不少,本文再也不贅述。值得一提的是,這個通用組件依然會面臨底層實現更換或者版本升級的問題,因此依照依賴倒置抽象出接口層的思路在這裏依然適用。

  • 緩存模塊

關於圖片的緩存在上文已經詳細討論。對於 JSON 格式的網絡請求結果,iOS 中通常使用 NSDictionary 存儲,它支持 NSCoding 協議,所以 YYCacheEGOCache等緩存框架都是可使用的。這部分的接口設計比較直白,爲指定 key 對應的值進行緩存,根據給定 key 返回對應的緩存值,以及移除給定 key 對應的內容。抽象接口層的思路,照例適用。

  • 下載管理業務模塊

在項目的不少地方可能都須要獲知當前下載模塊的狀態,因此這裏使用單例實現是一個比較好的選擇。在整個下載過程的最初,它根據提交的每個初始任務數據,解析出具體的子任務類型,調用對應的子模塊完成子任務的下載。同一下載任務下的各子任務之間應該是異步的,因此 dispatch group 是一個直觀的選擇。順序提交的全部初始任務之間,則是同步的關係,這裏可使用相似隊列的結構來管理。下面給出一個示意圖:

對於下載中、已下載這兩種狀態的區分,這裏提供一個改進思路:在某個初始任務真正開始下載以前,就向數據庫中插入一條新的下載記錄,設置狀態字段爲未完成,當全部子任務均完成且經過完整性校驗後,更新狀態字段爲完成。

最後,爲你們提供一個業務模塊的樣例僞代碼,用以展現整個下載流程。

//下載管理業務模塊的接口列表(大意展現)

//業務方的model
@class OriginModel;

@interface DownloadManager : NSObject
//獲取下載管理對象(單例)
+ (instancetype)sharedInstance;
//獲取下載中的任務
- (NSArray<OriginModel *> *)getDownloadingItems;
//獲取已下載的任務
- (NSArray<OriginModel*> *)getDownloadedItems;
//根據id獲取已下載的item
- (OriginModel *)getDownloadedItemById:(id<NSCopying>)itemId;
//是否下載過指定id的item
- (BOOL)didDownloadedItem:(id<NSCopying>)itemId;
//批量下載
- (void)downloadItems:(NSArray<OriginModel*> *)items;
//暫停下載
- (void)pauseDownloadForItem:(id<NSCopying>)itemId;
//恢復下載
- (void)resumeDownloadForItem:(id<NSCopying>)itemId;
//取消下載
- (void)cancelDownloadForItem:(id<NSCopying>)itemId;
@end
複製代碼
//下載管理業務模塊的主要實現

@implementation DownloadManager

- (void)downloadItems:(NSArray<OriginModel *> *)items {
    
// 解析任務結構,將全部任務push進任務隊列
    MissionStruct *oneStruct = [self analyzeMission];
    for (MissionItem *item in oneStruct) {
        [self.missionList pushItem:item];
    }
    ...
// 若非空,從任務隊列中取出任務元素
    if (![self.missionList isEmpty]) {
        MissionItem *oneMission = [self.missionList pop];
        [self handleMission:oneMission];
    }
}

- (void)handleMission:(MissionItem *)mission {
    
    // 調用數據庫模塊,插入一條新紀錄
    [DatabaseManager insertMission:mission];
    dispatch_group_t downloadGroup;
    
    // 下載視頻
    for (videoMission in mission.videos) {
        dispatch_group_enter(downloadGroup);
        // 調用文件管理模塊,獲取該url對應的文件路徑
        targetPath = [FileManager pathForURL:videoMission.url];
        // 調用大文件下載模塊,下載該視頻
        [FileDownloadManager downloadFile:videoMission.url
                               targetPath:targetPath
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // 下載音頻
    for (audioMission in mission.audios) {
        dispatch_group_enter(downloadGroup);
        // 調用文件管理模塊,獲取該url對應的文件路徑
        targetPath = [FileManager pathForURL:audioMission.url];
        // 調用大文件下載模塊,下載該音頻
        [FileDownloadManager downloadFile:audioMission.url
                               targetPath:targetPath
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // 緩存圖片
    for (imageMission in mission.images) {
        dispatch_group_enter(downloadGroup);
        // 調用圖片緩存模塊,緩存該圖片
        [ImageCacheManager cacheImage:imageMission.url
                                  success:^(){
                                      dispatch_group_leave(downloadGroup);
                                  }];
    }
    
    // 緩存網絡請求
    for (contentMission in mission.contents) {
        dispatch_group_enter(downloadGroup);
        // 調用網絡請求緩存模塊,緩存該網絡請求
        [RequestCacheManager cacheRequest:contentMission.url
                              success:^(){
                                  dispatch_group_leave(downloadGroup);
                              }];
    }
    
    ...
    
    // 全部子任務均完成
    dispatch_group_notify(downloadGroup, dispatch_get_global_queue(0, 0), ^{
    // 經過完整性校驗
        if ([self verifyAllSubMission:mission]) {
            // 調用數據庫模塊,更新該下載紀錄
            [DatabaseManager updateMission:mission];
        } else {
            // 未經過完整性校驗,移除數據庫對應記錄
            [DatabaseManager removeMission:mission];
        }
    });
}

@end
複製代碼

知識小集是一個團隊公衆號,主要定位在移動開發領域,分享移動開發技術,包括 iOS、Android、小程序、移動前端、React Native、weex 等。每週都會有 原創 文章分享,咱們的文章都會在公衆號首發。歡迎關注查看更多內容。

相關文章
相關標籤/搜索