Android動態加載入門 簡單加載模式

基本信息

初步瞭解Android動態加載

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

如何獲取可以加載的.dex文件

首先咱們能夠經過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解壓出來。服務器

加載並調用.dex裏面的方法

與JVM不一樣,Android的虛擬機不能用ClassCload直接加載.dex,而是要用DexClassLoader或者PathClassLoader,他們都是ClassLoader的子類,這二者的區別是框架

  1. DexClassLoader:能夠加載jar/apk/dex,能夠從SD卡中加載未安裝的apk;

  2. 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的動態升級了」。

如何動態更改XML佈局

雖然已經能動態更改代碼邏輯了,可是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資源框架不是一個很明智的作法,可是這種作法確實可行,在咱們以前的實際生產中的項目中也穩定使用着,這裏出於責任問題就不方便公開細節了。

(說實在,這種方案很是繁瑣,很差維護,一方面,這是產品一句「技術可行就作唄」而產生的解決方案;另外一方面,可是動態加載技術還很不成熟,也沒有什麼實際投入到生產的項目,因此採起了很是保守的開發方式)。

使用Fragment代替Activity

Activity須要在Manifest裏註冊,而後一標準的Intent啓動纔會具備生命週期,很明顯,若是想要動態加載的.dex裏的Activity沒有註冊的話,是沒法啓動的。

有一種簡單粗暴的作法就是能夠把.dex裏全部須要用到的Activity都事先註冊到原項目裏,不過這樣一來若是.dex裏的Activity有變化,原項目就必須跟着升級。

另一種方案是使用Fragment,Fragment自帶生命週期,不須要在Manifest裏註冊,因此能夠在.dex裏使用Fragment來代替Activity,代價就是Fragment之間的切換會繁瑣許多。

ART模式的兼容性問題

當初咱們開始設計動態加載方案的時候,尚未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動態性加載初級階段」的解決方案,雖然如今已經能投入到具體的生產中去,可是還有一些問題沒法忽略。

  1. 沒法使用res目錄下的資源,特別是使用XML佈局,以及沒法經過res資源到達自適應

  2. 沒法動態加載新的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

相關文章
相關標籤/搜索