【動態加載】Android動態加載:簡單加載模式

初步瞭解Android動態加載

Java程序中,JVM虛擬機是通過類加載器ClassLoader加載.jar文件裏面的類的。Android也類似,不過Android用的是Dalvik/ART虛擬機,不是JVM,也不能直接加載.jar文件,而是加載dex文件。

先要通過Android SDK提供的DX工具把.jar文件優化成.dex文件,然後Android的虛擬機才能加載。注意,有的Android應用能直接加載.jar文件,那是因爲這個.jar文件已經經過優化,只不過後綴名沒改(其實已經是.dex文件)。

如果對ClassLoader的工作機制有興趣,具體過程請參考 Android 動態加載基礎 ClassLoader工作機制,這裏不再贅述。

如何獲取能夠加載的.dex文件

首先我們可以通過JDK的編譯命令javac把Java代碼編譯成.class文件,再使用jar命令把.class文件封裝成.jar文件,這與編譯普通Java程序的時候完全一樣。

之後再用Android SDK的DX工具把.jar文件優化成.dex文件(在“android-sdk\build-tools\具體版本\”路徑下)

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的方式解決,可以把這種的動態加載框架成爲“代理模式”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章