貨拉拉 Android 動態(tài)資源管理系統(tǒng)原理與實踐(上)

點擊上方藍字關(guān)注我,知識會給你力量

?jary,貨拉拉高級客戶端工程師,目前負責貨拉拉App Android端穩(wěn)定性提升,包體積優(yōu)化相關(guān)工作。
?
前言
隨著公司業(yè)務(wù)的擴展,貨拉拉用戶端apk包的體積也不斷變大,過去一年,用戶端android組進行了大量的瘦身工作,取得了較為顯著的成果。再使用常規(guī)方法,已經(jīng)很難優(yōu)化包體積了。 我們可以把一些使用頻率相對較低的資源不打包進apk,并在需要時下載到本地(例如動畫文件,字體,zip壓縮包,so庫等) 我們注意到,貨拉拉用戶端apk中,使用了35個以上的so庫,并且都支持arm64-v8a和armeabi-v7a這2種abi,結(jié)果就是so體積成倍上漲。用戶端生產(chǎn)環(huán)境下的apk,解壓縮后,存放so包的lib目錄,占據(jù)了整個應(yīng)用41%的大小。 因此動態(tài)資源管理系統(tǒng)是下一個優(yōu)化的重點,動畫,字體和zip包只是普通文件,完全可以支持動態(tài)下載并使用。而so文件本質(zhì)上就是一種可動態(tài)加載并執(zhí)行的文件,將 so文件動態(tài)下發(fā)是切實可行的,但是要將它從 apk中剔除并保證穩(wěn)定性并不是一件易事。
行業(yè)方案
未找到現(xiàn)成的github項目或者三方sdk方案,來實現(xiàn)動態(tài)資源管理。 部分博客提供了動態(tài)管理so文件的思路,但是缺少完整流程。 行業(yè)目前并未提供完整的成熟方案供我們使用,需要我們自己造輪子。
功能和方案
實現(xiàn)功能
資源分類,預定義了字體,幀動畫,so這3種內(nèi)置資源,以及單個文件,多個文件這2種可自定義資源。 提供通用的加載動態(tài)資源方法,所有資源均可由此加載。 內(nèi)置資源,提供默認的應(yīng)用方法,外部可以直接應(yīng)用。自定義資源,用戶自行決定如何應(yīng)用。 對于所有資源,提供可配置的方便快捷打包方式,減少手動操作。

幾個概念
資源加載:將動態(tài)資源通過下載,校驗,解壓等方式,映射到本地文件的過程。
該過程對所有資源通用,sdk使用方無需修改資源加載方式。
資源應(yīng)用:動態(tài)資源對應(yīng)的本地文件應(yīng)用到具體業(yè)務(wù)中。例如動態(tài)字體資源的應(yīng)用,就是為TextView設(shè)置一個新的字體。
該過程每個資源不同,sdk使用方無需修改內(nèi)置資源的應(yīng)用方式,對于自定義資源,需要使用方自行決定應(yīng)用方式。
資源打包:包括生成一個待上傳的資源文件,以及生成資源的Java描述(DynamicPkgInfo類)。so資源還包含了一些方法的hook操作。
該過程對所有資源都適用,統(tǒng)一使用可配置的dynamic_plugin插件完成。sdk使用方無需修改資源打包方式,但是可通過配置dyanmic_plugin.gradle文件,配置打包過程。
通用資源加載
如何確定資源已經(jīng)下載過了,避免重復下載?
Java代碼中,使用DynamicPkgInfo類來描述資源,該類中包含了資源的版本號。我們比較該類和本地數(shù)據(jù)庫中的資源版本號,如果不同,才會下載資源。
下載資源是否提供多線程下載,斷點續(xù)傳等功能?
本sdk只提供了下載接口,未提供實際下載功能,因此如需這些功能,需要調(diào)用者自己實現(xiàn)。
如何校驗資源,防止被篡改?
DynamicPkgInfo類中包含了資源校驗信息,我們利用該類,對下載好的文件進行md5碼,文件長度,文件名稱的校驗。
如何判斷資源是否壓縮包,以及如何解壓縮?
目前簡單的采用后綴名是否為.zip判斷,使用使用Java內(nèi)置java.util.zip包下工具解壓。
如何校驗解壓后的資源子文件,防止被篡改?
DynamicPkgInfo同樣包含了zip包中所有子文件的校驗信息,我們利用它,來校驗所有解壓后的文件。
資源應(yīng)用
字體資源應(yīng)用,從加載好的本地文件中,創(chuàng)建系統(tǒng)Typeface字體對象,并設(shè)置到TextView上。 幀動畫資源應(yīng)用,從加載好的本地文件中,創(chuàng)建系統(tǒng)AnimationDrawable幀動畫對象,并設(shè)置到ImageView上。 字體和幀動畫資源的應(yīng)用流程,見第5章,內(nèi)置資源應(yīng)用流程。 so資源應(yīng)用流程,見第7章,so資源加載和應(yīng)用解決方案。 自定義資源的應(yīng)用,需要sdk使用者自己定義。 資源打包
我們使用dynamic_plugin gradle插件來完成所有資源的打包。
字體資源打包
掃描輸入目錄字體文件,將他們拷貝到輸出目錄。 為每個字體生成一個DynamicPkgInfo類的常量,代表該動態(tài)資源。 幀動畫資源打包
掃描輸入目錄幀動畫文件夾,將它們逐個壓縮,并將壓縮包輸出到指定目錄。 為每一組幀動畫生成一個DynamicPkgInfo類的常量,代表該動態(tài)資源。 so資源打包
Hook系統(tǒng)System.loadLibrary方法的調(diào)用。 系統(tǒng)打包流程中,刪除配置文件指定so文件,并將他們拷貝到指定目錄。 掃描上面的so文件目錄,將他們逐個壓縮,并將壓縮包輸出到指定目錄。 為每一個so壓縮包生產(chǎn)一個DynamicPkgInfo類的常量,代表該動態(tài)資源。 自定義資源打包
單個文件的資源打包同字體資源 多個文件的資源打包同幀動畫資源 運行產(chǎn)物
下圖為該打包插件運行一次之后的產(chǎn)物。 input目錄,所有待打包資源的存放目錄,我們需要手動把要打包的資源拷貝這里,例如字體文件拷貝到input/typeface目錄下。注意so資源會在打包過程中,自動生成,無需手動處理。 output目錄,則是打包出來的產(chǎn)物,包括字體資源,so資源,幀動畫資源等,我們可以手動將此目錄下的打包后資源上傳到服務(wù)器。 DynamicResConst.java文件,該文件中生成了所有資源的信息。

DynamicResConst.java文件的內(nèi)容,我們在這里也稍微看一下,圖中為字體資源和幀動畫資源的java描述。可以看到所有動態(tài)資源,都用DynamicPkgInfo類來描述。 單個文件資源,包含了資源的id,文件名稱,資源類型,下載地址,版本號,文件長度以及md5碼。 多個文件資源,除了包含上述信息外。還包含了該壓縮包解壓后,里面每個文件的名稱,文件長度以及md5碼

整體架構(gòu)
由于整個系統(tǒng)功能較復雜,我們將其分為3個module。
lib_dynamic_base:只包含md5,壓縮解壓等通用操作以及代表資源的實體類DynamicPkgInfo,該module為后面2個module的基礎(chǔ)。 lib_dynamic_res:提供了資源的加載和應(yīng)用功能,目前包含字體資源,幀動畫資源,so資源以及自定義資源。 dynamic_plugin:為一個gradle plugin工程,提供了資源打包功能。

lib_dynamic_res模塊架構(gòu)
該庫包括了動態(tài)資源加載和應(yīng)用全過程,我們分為5層實現(xiàn)
外部接口層,主要為加載管理器和加載監(jiān)聽器,提供了所有外部的接口。 資源應(yīng)用層,封裝了幾種內(nèi)置動態(tài)資源的應(yīng)用,字體資源,幀動畫資源,so資源。 加載流程層,具體完成了資源的加載過程,主要采用狀態(tài)模式實現(xiàn),包括一個狀態(tài)管理器,以及各種狀態(tài),例如檢查本地版本狀態(tài),下載狀態(tài),校驗文件狀態(tài)等。 接口隔離層,主要是一些功能接口,例如下載功能,解壓縮功能,上報功能等,隔離了底層實現(xiàn)。 具體實現(xiàn)層,各個具體功能的實現(xiàn),例如數(shù)據(jù)庫操作,java zip庫等。

dynamic_plugin插件架構(gòu)
系統(tǒng)插件層,主要為系統(tǒng)gradle plugin的實現(xiàn),以及對dynamic_plugin.gradle配置文件的讀取和解析 任務(wù)模塊層,包含了各個任務(wù),例如刪除并拷貝so文件任務(wù),壓縮zip包任務(wù)等。 底層實現(xiàn)層,包含了具體功能的實現(xiàn),例如asm框架和transform api,zip壓縮,javepoet代碼生成等。

通用資源加載,內(nèi)置資源應(yīng)用流程
通用資源加載主流程
加載普通資源的主流程如下,首先判斷資源包指定版本號和本地數(shù)據(jù)庫版本號是否相同,如果想同,進入本地資源校驗流程,否則進入下載流程。

下載校驗解壓流程
我們首先調(diào)用下載接口下載資源。 如果下載成功,我們校驗下載文件,下載失敗,則嘗試刪除文件,并直接跳到失敗結(jié)果。 校驗下載文件成功,我們在判斷是否為zip文件,對于zip文件,我們執(zhí)行解壓縮操作,非zip文件,直接成功。 解壓縮完成后,我們在對解壓后的所有文件執(zhí)行校驗操作。

本地資源校驗流程
對于下載并解壓的壓縮包資源,以及本地數(shù)據(jù)庫版本和資源實體類版本號相同的資源,我們需要進行本地資源校驗流程。 遍歷資源包指定的字文件列表,對他們進行逐個文件檢驗就可以了。

單個文件校驗流程
資源實體類中指定的文件名稱,文件長度,文件md5碼和本地文件相同時,我們認為該文件校驗成功了

加載恢復流程
動態(tài)資源加載過程中,可能因為各種原因,導致加載未能得到成功或者失敗的結(jié)果,而在中間狀態(tài)被中斷,如應(yīng)用進程被殺死,手機關(guān)機等等。為了避免加載意外中斷的情況下,完全從頭開始進行加載,我們設(shè)計了一個動態(tài)資源加載的恢復流程,如果異常中斷,我們下次加載資源時,可以恢復到當前狀態(tài),繼續(xù)進行加載。
下載過程的恢復和斷點續(xù)傳,需要下載接口的實現(xiàn)者負責。 其他狀態(tài),我們在狀態(tài)改變時,將資源id,當前狀態(tài)和待處理文件路徑,保存到數(shù)據(jù)庫。 每次加載動態(tài)開始時,根據(jù)資源id查找數(shù)據(jù)庫中是否有待恢復數(shù)據(jù)。 有待恢復數(shù)據(jù),轉(zhuǎn)到待恢復的狀態(tài),否則,直接去檢查版本號狀態(tài)。 資源加載成功或者失敗時,從數(shù)據(jù)庫中刪除當前資源id對應(yīng)的恢復狀態(tài)。

內(nèi)置資源應(yīng)用流程
前面我們總結(jié)了動態(tài)資源的加載流程,資源加載完成后,我們還需要將該資源進行應(yīng)用,而這里我們要說的就是將動態(tài)資源應(yīng)用到對應(yīng)View上的流程。
根據(jù)資源id,從緩存中獲取動態(tài)資源對應(yīng)的本地文件。 文件獲取成功,直接設(shè)置到view上,獲取失敗,進入下一步。 參數(shù)列表中的占位資源不為空,則將占位資源設(shè)置到View上。 將資源id設(shè)置到View的tag上,嘗試清除上次動態(tài)資源加載失敗狀態(tài)。 使用管理器Manager類的load方法,執(zhí)行之前的加載流程。 異步等待加載完成回調(diào),判斷資源id是否和View的tag相同,防止view被復用,導致的資源錯亂情況。 如果Activity沒有被銷毀,則將資源設(shè)置到View上。

lib_dynamic_res模塊類設(shè)計
可與第4章,整體架構(gòu)分層圖對照著看
外部接口層
DynamicResManager類負責和外部交互,提供了初始化(init),加載資源(load),isResReady(判斷資源是否就緒),clearFailState(清除錯誤狀態(tài)等方法)等方法。
Config類,則可以向管理器提供線程池,下載器接口,本地資源信息接口,本地資源狀態(tài)接口等配置信息。
AbsResInfo抽象類,代表動態(tài)資源。
DynamicPkgInfo類,AbsResInfo的子類,提供給外部使用,代表了一個動態(tài)資源實體。
DynamicPkgInfo.FileInfo,AbsResInfo的子類,資源實體內(nèi)部類,代表了資源中的一個子文件。
DynamicPkgInfo.FolderInfo,AbsResInfo的子類,資源實體內(nèi)部類,代表了資源中的一個子文件夾。
ILoadResListener接口,提供了加載資源時的回調(diào)功能,會回調(diào)加載成功,失敗,狀態(tài)變化,下載中進度
資源應(yīng)用層
AbsResApply抽象類,實現(xiàn)了動態(tài)資源在ui元素上的應(yīng)用。
TypefaceResApply類,AbsResApply的子類,代表了字體資源的應(yīng)用。
FrameAnimApply類,AbsResApply的子類,代表了幀動畫資源的應(yīng)用。
AbsSoLoad抽象類,實現(xiàn)了so動態(tài)資源的應(yīng)用。
RelinkerSoLoad類,AbsSoLoad的子類,使用Relinker第三方庫最終load so庫。
SystemSoLoad類,AbsSoLoad的子類,使用系統(tǒng)System.loadLibrary方法最終load so庫
加載流程層
我們使用狀態(tài)模式來控制整個動態(tài)資源的加載流程。
IState,狀態(tài)接口,代表了加載流程中的一個狀態(tài)。
InitState類,初始化狀態(tài)。
CheckVersionState類,檢查資源實體類版本號與數(shù)據(jù)庫版本號是否相同狀態(tài)。
DownloadState類,下載資源狀態(tài)。
VerrifyFileState,校驗下載資源狀態(tài)。
UnZipState,解壓縮下載資源狀態(tài)。
VerifyZipState,校驗解壓后的所有文件狀態(tài)。
IStateMechine,狀態(tài)管理機接口,負責管理前面所有的IState對象。
DefaultStateMachine類,狀態(tài)管理機的默認實現(xiàn)。
ResCtx類,狀態(tài)管理機運行過程中的全局context對象,存儲了路徑信息,加載成功信息,加載失敗異常等全局信息。
接口隔離和具體實現(xiàn)層
這2層的類,較為雜亂,限于篇幅,我們就不一一列舉了。
類uml圖

……未完待續(xù)……
向大家推薦下我的網(wǎng)站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
