做者:kaedeahtml
項目:android-dynamical-loadingjava
Java程序中,JVM虛擬機是經過類加載器ClassLoader加載.jar文件裏面的類的。Android也相似,不過Android用的是Dalvik/ART虛擬機,不是JVM,也不能直接加載.jar文件,而是加載dex文件。android
先要經過Android SDK提供的DX工具把.jar文件優化成.dex文件,而後Android的虛擬機才能加載。注意,有的Android應用能直接加載.jar文件,那是由於這個.jar文件已經通過優化,只不事後綴名沒改(其實已是.dex文件)。git
若是對ClassLoader的工做機制有興趣,具體過程請參考 Android 動態加載基礎 ClassLoader工做機制,這裏再也不贅述。github
首先咱們能夠經過JDK的編譯命令javac把Java代碼編譯成.class文件,再使用jar命令把.class文件封裝成.jar文件,這與編譯普通Java程序的時候徹底同樣。編程
以後再用Android SDK的DX工具把.jar文件優化成.dex文件(在「android-sdk\build-tools\具體版本\」路徑下)segmentfault
dx --dex --output=target.dex origin.jar // target.dex就是咱們要的了緩存
此外,咱們能夠現把代碼編譯成APK文件,再把APK裏面的.dex文件解壓出來,或者直接把APK文件當成.dex使用(只是APK裏面的靜態資源文件咱們暫時還用不到)。至此咱們發現,不管加載.jar,仍是.apk,其實都和加載.dex是等價的,Android能加載.jar和.apk,是由於它們都包含有.dex,直接加載.apk文件時,ClassLoader也會自動把.apk裏的.dex解壓出來。服務器
與JVM不一樣,Android的虛擬機不能用ClassCload直接加載.dex,而是要用DexClassLoader或者PathClassLoader,他們都是ClassLoader的子類,這二者的區別是框架
DexClassLoader:能夠加載jar/apk/dex,能夠從SD卡中加載未安裝的apk;
PathClassLoader:要傳入系統中apk的存放Path,因此只能加載已經安裝的apk文件;
使用前,先看看DexClassLoader的構造方法
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); }
注意,咱們以前提到的,DexClassLoader並不能直接加載外部存儲的.dex文件,而是要先拷貝到內部存儲裏。這裏的dexPath就是.dex的外部存儲路徑,而optimizedDirectory則是內部路徑,libraryPath用null便可,parent則是要傳入當前應用的ClassLoader,這與ClassLoader的「雙親代理模式」有關。
實例使用DexClassLoader的代碼
File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test_dexloader.jar");// 外部路徑 File dexOutputDir = this.getDir("dex", 0);// 沒法直接從外部路徑加載.dex文件,須要指定APP內部路徑做爲緩存目錄(.dex文件會被解壓到此目錄) DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(), null, getClassLoader());
到這裏,咱們已經成功把.dex文件給加載進來了,接下來就是如何調用.dex裏面的代碼,主要有兩種方式。
使用DexClassLoader加載進來的類,咱們本地並無這些類的源碼,因此沒法直接調用,不過能夠經過反射的方法調用,簡單粗暴。
DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader()); Class libProviderClazz = null; try { libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader"); // 遍歷類裏全部方法 Method[] methods = libProviderClazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { Log.e(TAG, methods[i].toString()); } Method start = libProviderClazz.getDeclaredMethod("func");// 獲取方法 start.setAccessible(true);// 把方法設爲public,讓外部能夠調用 String string = (String) start.invoke(libProviderClazz.newInstance());// 調用方法並獲取返回值 Toast.makeText(this, string, Toast.LENGTH_LONG).show(); } catch (Exception exception) { // Handle exception gracefully here. exception.printStackTrace(); }
畢竟.dex文件也是咱們本身維護的,因此能夠把方法抽象成公共接口,把這些接口也複製到主項目裏面去,就能夠經過這些接口調用動態加載獲得的實例的方法了。
pulic interface IFunc{ public String func(); } // 調用 IFunc ifunc = (IFunc)libProviderClazz; String string = ifunc.func(); Toast.makeText(this, string, Toast.LENGTH_LONG).show();
到這裏,咱們已經成功從外部路徑動態加載一個.dex文件,並執行裏面的代碼邏輯了。經過從服務器下載最新的.dex文件並替換本地的舊文件,就能初步實現「APP的動態升級了」。
雖然已經能動態更改代碼邏輯了,可是UI界面要怎麼更改啊?Android開發中大部分的狀況下,UI界面都是經過XML佈局實現的,放在res目錄下,但是.dex庫裏面並無這些靜態資源啊,因此沒法改變XML佈局。(這裏即便直接動態加載APK文件,可是經過DexClassLoader只能加載新的APK其中的.dex文件,並沒有法加載其中的res資源文件,因此若是在動態加載的.dex中直接使用新的APK的res資源的話會拋出異常。)
你們都知道,全部的XML佈局在運行的時候都要經過LayoutInflator渲染成View的實例,這個實例與咱們使用純Java代碼建立的View實例幾乎是等價的,並且後者可能效率還更高,全部的XML佈局實現的UI界面都有等價的純代碼的建立方案。由此伸展開來,res目錄下全部XML資源都有等價的純代碼的實現方式,好比XML動畫、XML Drawable等。
因此,若是想要動態更改應用的UI界面的話,能夠經過用純代碼建立佈局的形式來解決。此外,還能夠模仿LayoutInflator的工做方式,本身寫一套佈局解析器來解析XML文件,這樣就能在徹底不依賴res資源的狀況下建立UI界面了,固然這樣的工做量很多,並且,徹底避開res資源的話,全部的分辨率、國際化等自適應問題都要本身在應用層寫代碼維護了,顯然脫離res資源框架不是一個很明智的作法,可是這種作法確實可行,在咱們以前的實際生產中的項目中也穩定使用着,這裏出於責任問題就不方便公開細節了。
(說實在,這種方案很是繁瑣,很差維護,一方面,這是產品一句「技術可行就作唄」而產生的解決方案;另外一方面,可是動態加載技術還很不成熟,也沒有什麼實際投入到生產的項目,因此採起了很是保守的開發方式)。
Activity須要在Manifest裏註冊,而後一標準的Intent啓動纔會具備生命週期,很明顯,若是想要動態加載的.dex裏的Activity沒有註冊的話,是沒法啓動的。
有一種簡單粗暴的作法就是能夠把.dex裏全部須要用到的Activity都事先註冊到原項目裏,不過這樣一來若是.dex裏的Activity有變化,原項目就必須跟着升級。
另一種方案是使用Fragment,Fragment自帶生命週期,不須要在Manifest裏註冊,因此能夠在.dex裏使用Fragment來代替Activity,代價就是Fragment之間的切換會繁瑣許多。
當初咱們開始設計動態加載方案的時候,尚未ART模式。隨着Kitkat的發佈以及ART模式的出現,咱們開始擔憂「用DexClassLoader加載.dex文件」的方案會不會在ART模式上面存在兼容性問題。
其實,ART模式相比原來的Dalvik,會在安裝APK的時候,使用Android系統自帶的dex2oat工具把APK裏面的.dex文件轉化成OAT文件,OAT文件是一種Android私有ELF文件格式,它不只包含有從DEX文件翻譯而來的本地機器指令,還包含有原來的DEX文件內容。這使得咱們無需從新編譯原有的APK就可讓它正常地在ART裏面運行,也就是咱們不須要改變原來的APK編程接口。ART模式的系統裏,一樣存在DexClassLoader類,包名路徑也沒變,只不過它的具體實現與原來的有所不一樣,可是接口是一致的。
package dalvik.system; import dalvik.system.BaseDexClassLoader; import java.io.File; public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); } }
也就是說,ART模式在加載.dex文件的方法上,對Dalvik作了向下兼容,因此使用DexClassLoader加載進來的.dex文件一樣也會被轉化成OAT文件再被執行,「以DexClassLoader爲核心的動態加載方案」在ART模式上能夠穩定運行。
關於ART模式以及OAT文件的詳細分析,請參考官方的ART and Dalvik,以及老羅的Android ART運行時無縫替換Dalvik虛擬機的過程分析。
以上大體就是「Android動態性加載初級階段」的解決方案,雖然如今已經能投入到具體的生產中去,可是還有一些問題沒法忽略。
沒法使用res目錄下的資源,特別是使用XML佈局,以及沒法經過res資源到達自適應
沒法動態加載新的Activity等組件,由於這些組件須要在Manifest中註冊,動態加載沒法更改當前APK的Manifest
以上問題能夠經過反射調用Framework層代碼以及代理Activity的方式解決,能夠把這種的動態加載框架成爲「代理模式」。
http://44289533.iteye.com/blog/1954453
http://blog.csdn.net/bboyfeiyu/article/details/11710497
http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html