基于DX的安卓動態更新報告 [復制鏈接]

2018-9-18 10:53
Mob開發者平臺 閱讀:676 評論:0 贊:0
Tag:  

DX簡介

安卓程序的主要代碼是java 代碼,不過由于安卓系統不直接使用sun的jvm,所以從javac編譯過來的class文件并不能直接被安卓系統加載運行。

要想運行java代碼,需要除了和以前一樣調用javac將java代碼編譯為class文件以外,還需要調用dx這個工具,將class文件轉成dex文件。然后安卓系統才會去加載dex文件并執行程序。因此說,dx是將class文件(及class文件集合)轉成dex的工具。

dx屬于安卓開源項目的一部分,它的源碼位于“external/dexmaker”目錄下,純java。由于是純java工具,所以可以打包成依賴庫放到我們的 項目里面,實現安卓平臺上,將一組class文件或一個jar文件轉換成dex的功能。下面我將通過代碼演示將一組class文件編譯成dex,并加載運 行。

使用DX

  • 導入class文件

程序的第一步是將class文件以字節數組的方式讀入內存。為了演示方便,這里我將運行時編譯的class集合選擇為MobTools,為了簡化示例程序,我事先將它解壓到項目的assets中。下面的代碼演示的是以遞歸的方式讀取所有class文件到內存中:

  • 初始化dex配置信息

載入class文件后,就可以將它們寫入dex,但是dex會有一些配置信息需要初始化,下面的代碼演示了這個過程:

我并沒有仔細查閱過這兩個類的用途,希望后續使用此功能的同事可以追加注釋。

  • 添加class文件

接下來是將class類名和數據添加到dex實例中:

DexFile是dex數據在內存中的抽象,可以理解為一個列表。列表中存放著一個個ClassDefItem,攜帶著各個class的完整路徑和描述數據。

我在添加的過程中利用ClassDefItem對類描述數據的解析功能,獲取了每一個類的父類和接口列表,并緩存了起來。這個會在后續加載dex的時候用到。

  • 保存dex文件

DexFile是dex文件在內存的解析結果,不能直接拿來使用。安卓系統加載dex都是基于文件的,所以現在還需要將DexFile轉成dex數據:

toDex的第一個參數是日志輸出工具,如果設置為null,表示不輸出日志。

完成轉換以后得到一個byte數組,只要將byte數組寫入文件,并保存為dex后綴就可以被多數的安卓系統加載。但是對于一些定制的系統,比方說小米,他 們只識別壓縮包里的dex,比方說jar包里的dex。這種方式普通系統也是支持的,為了更好的兼容性,我將dex文件保存到了應用私有目錄下的一個 jar文件中:

數據會保存到“/data/data/包名/files”下的MobTools.jar中。打開個jar包,里面就是我們熟悉的“classes.dex”文件。

  • 加載dex文件

有了文件,現在就可以調用系統的api來加載它:

dalvik.system.DexFile是android.jar里面提供的api,雖然名字也是DexFile,但是功能和dx里面DexFile不同。使用時通過其靜態方法loadDex來獲取實例。傳入的參數jarPath表示jar包的路徑,dexPath表示準備加壓的dex文件路徑。

由于MobTools中有很多各類,而且不同的類有繼承關系,所以這里我開辟了一個方法loadClass做遞歸加載。其原理是:循環獲取 classesNames列表中的第一個元素嘗試進行加載,加載前判斷這個類是否有父類,如果有,將這個父類從classesNames列表中取出,然后先對這個父類進行加載。如果被加載的類沒有父類,則再檢查他是否有實現什么借口,如果有,循環加載這些接口的類。父類和接口列表都完成加載以后,再加載最 初嘗試加載的類。這樣子就不會再加載的過程中報出“子類找不到父類”的問題。

使用

完成了上述的操作后,原先assets中的class文件就被全部加載如內容,可以被使用了。

使用的方式有兩種,一直是基于反射,先獲取某個類的Class 對象,然后通過Class對象獲取方法或字段,最后invoke或者get/set。第二種方式是實現通過依賴mobtools的方式編寫一份代碼,并將 這份代碼編譯為jar包,被我們的項目依賴;等到mobtools加載完畢后,代用這個jar包中的api,使用其中的功能。

這兩種辦法各有優劣。第一種辦法編寫起來很麻煩,容易出錯,但是隨時可以改變邏輯。第二種辦法由于編碼的時候依賴了mobtools,所以能以常規的編碼方式進行開發,后來使用時調用它的api時也比較直觀。但是它在使用時已經是靜態的jar包,要改動比較麻煩。

動態更新

我研究dx的目的在于動態更新,否則在安卓系統上編譯和加載class文件并沒有什么意義。一般操作方式是這樣子:首先,對于一些可能日后要改變的功能,編 譯代碼時不將它們打包到apk的dex中,而是打包到zip文件,當做資源的方式隨包發布;使用時會對zip文件進行解壓,然后運行時合并為dex,并 path到內存中使用。然后,等項目的邏輯改變了,服務端可以向客戶端推送改變邏輯后的class文件集合,去覆蓋原來的class文件;等到下一次客戶 端重新啟動時,發現class文件版本已經改變了,于是再度動態合并dex,并path。

如此,apk的升級可以變得頻繁,而且每次都是增量更新。如果用戶刪除了緩存的數據,應用只會恢復到初始狀態,然后再從服務端獲取最新版本的“class補丁”替換合并即可。

甚至,如果服務端的性能更強的情況下,可以對不同的客戶端下發不同的class文件,比方說為不同的客戶端定制RSA的公鑰,或者其它的配置信息等等。

與groovy結合

如果嫌以class為單位來更新代碼,顆粒度還不夠的話,還可以結合groovy的解析器。

groovy 是一種腳本語言,它基于java虛擬機,并且完全兼容java語法。groovy能將自己的腳本編譯為java的class,所以剛好可以被我們用來減少 動態更新的顆粒度。比方說一開始我們發布的時候不是將class文件壓縮為一個zip,而是直接壓縮java源碼。運行后調用groovy來解析這些 java源碼,將它們編譯為class文件,再調用dx對這些class文件進行合并。升級的時候,服務端指定具體的源碼文件的具體段落進行替換,可以替 換一整個包,也可以只是替換一行代碼。替換以后再調用groovy重新編譯即可。


文/Mob開發者平臺 技術副總監 余勛杰


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期