Android動態加載機制

Android能夠實現動態加載機制,得益於java虛擬機團隊設計的類加載,把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到Java虛擬機外部去實現,以便讓應用程序自己決定如何獲取所需要的的類,實現這個動作的代碼模塊稱爲“類加載器”(參考《深入理解Java虛擬機–JVM高級特性與最佳實踐》7.4類加載器)。

一、Android中的ClassLoader

Java中的ClassLoader是加載class文件,而Android中的虛擬機無論是dvm還是art都只能識別dex文件。因此Java中的ClassLoader在Android中不適用。Android中的java.lang.ClassLoader這個類也不同於Java中的java.lang.ClassLoader。
爲什麼Android要自創dex,而不用java的class?

  • dvm是基於寄存器的虛擬機 而jvm執行是基於虛擬棧的虛擬機。寄存器存取速度比棧快的多,dvm可以根據硬件實現最大的優化,比較適合移動設備。
  • 傳統Class文件是一個Java源碼文件會生成一個.class文件,而Android是把所有Class文件進行合併,優化,然後生成一個最終的class.dex,目的是把不同class文件重複的東西只需保留一份,如果我們的Android應用不進行分dex處理,最後一個應用的apk只會有一個dex文件。

Android中的ClassLoader類型也可分爲系統ClassLoader和自定義ClassLoader。其中系統ClassLoader包括3種分別是:

  • BootClassLoader,Android系統啓動時會使用BootClassLoader來預加載常用類,與Java中的Bootstrap ClassLoader不同的是,它並不是由C/C++代碼實現,而是由Java實現的。BootClassLoader是ClassLoader的一個內部類。
  • PathClassLoader,全名是dalvik/system.PathClassLoader,可以加載已經安裝的Apk,也就是/data/app/package 下的apk文件,也可以加載/vendor/lib, /system/lib下的nativeLibrary。
  • DexClassLoader,全名是dalvik/system.DexClassLoader,可以加載一個未安裝的apk文件。

在MainActivity中打印當前的ClassLoader,

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            System.out.println("classLoader: " + classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

結果如下:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sososeen09.classloadtest-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
java.lang.BootClassLoader@aced87d

從打印的結果也可以證實:App系統類加載器是PathClassLoader,而BootClassLoader是其parent類加載器。

二、ClassLoader分析
在Android中我們主要關心的是PathClassLoader和DexClassLoader。
PathClassLoader和DexClasLoader都是繼承自 dalviksystem.BaseDexClassLoader,它們的類加載邏輯全部寫在BaseDexClassLoader中。PathClassLoader用來操作本地文件系統中的文件和目錄的集合。DexClassLoader可以加載一個未安裝的APK,也可以加載其它包含dex文件的JAR/ZIP類型的文件。DexClassLoader需要一個對應用私有且可讀寫的文件夾來緩存優化後的class文件。而且一定要注意不要把優化後的文件存放到外部存儲上,避免使自己的應用遭受代碼注入攻擊。
Android中具體負責類加載的並不是哪個ClassLoader,而是通過DexFile的defineClassNative()方法來加載的。
在這裏插入圖片描述三、動態加載(熱修復)實例
1、新建一個module叫dextest,加入這就是我們要動態加載進來的模塊。
裏面新建一個叫做ShowToast 的類:

public class ShowToast {

    public void showToast(Context context) {
        Toast.makeText(context, "動態加載ShowToast", Toast.LENGTH_SHORT).show();
    }
}

ShowToast 類裏面只有一個showToast方法,該方法需要一個context參數,展示一個toast。
2、把java類打包到dex中
編譯之後,在如圖所示的目錄(不同版本的android studio中可能路徑會不一樣)中會生成相應的class文件,由於class文件是java虛擬機所用的,但是在安卓中的Dalvik虛擬機要使用dex文件,所以我們需要再進行轉換。
在這裏插入圖片描述還是這個module中的build.gradle文件最後添加:

//刪除isshowtoast.jar包任務
task clearJar(type: Delete) {
    delete 'build/libs/in.jar'
}
task makeJar(type:org.gradle.api.tasks.bundling.Jar){
    //指定生成的jar名
    baseName 'in'
    //從哪裏打包class文件
    from('build/intermediates/javac/debug/classes/com/example/dextest/')
    //打包到jar後的目錄結構
    into('com/example/dextest/')
    //去掉不需要打包的目錄和文件
    exclude('test/','BuildConfig.class','R.class')
    //去掉R$開頭的文件
    exclude{it.name.startsWith('R$')}
}
makeJar.dependsOn(clearJar,build)

這樣就把我們需要的文件打包到jar包中了,然而jar包並不是我們需要的,繼續。
在Android的SDK中爲我們提供了一個dx命令(在\android-sdk\build-tools\version[23.0.1] 或 \android-sdk\platform-tools下能找到);命令使用方式爲:dx --dex --output=out.jar in.jar,該命令將包含class的in.jar轉化爲包含dex的out.jar文件。
3、app端調用
把生成的out.jar包複製到app項目中的assets目錄下:
在這裏插入圖片描述我們這裏是爲了方便,真是場景可能是遠程服務器下載到指定位置。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadDex();
            }
        });
    }

    /**
     * 點擊事件
     */
    public void loadDex() {
        File cacheFile = getDir("dex",0);
        String internalPath = cacheFile.getAbsolutePath() + File.separator + "out.jar";
        File desFile=new File(internalPath);
        try {
            if (!desFile.exists()) {
                desFile.createNewFile();
                FileUtils.copyFiles(this,"out.jar",desFile);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        //下面開始加載dex class
        //1.待加載的dex文件路徑,如果是外存路徑,一定要加上讀外存文件的權限,
        //2.解壓後的dex存放位置,此位置一定要是可讀寫且僅該應用可讀寫
        //3.指向包含本地庫(so)的文件夾路徑,可以設爲null
        //4.父級類加載器,一般可以通過Context.getClassLoader獲取到,也可以通過ClassLoader.getSystemClassLoader()取到。
        DexClassLoader dexClassLoader=new DexClassLoader(internalPath,cacheFile.getAbsolutePath(),null,getClassLoader());
        try {
            Class clz = dexClassLoader.loadClass("com.example.dextest.ShowToast");
            Method method = clz.getDeclaredMethod("showToast", Context.class);
            method.invoke(clz.newInstance(), this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

這裏的FileUtils文件就是把jar包從assets目錄拷貝到app的data目錄下:

public class FileUtils {
    // 把assets目錄的文件複製到desFile中
    public static void copyFiles(Context context, String fileName, File desFile){
        InputStream in=null;
        OutputStream out=null;

        try {
            in=context.getApplicationContext().getAssets().open(fileName);
            out=new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes=new byte[1024];
            int len=0;
            while ((len=in.read(bytes))!=-1)
                out.write(bytes,0,len);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (in!=null)
                    in.close();
                if (out!=null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

一開始在這裏我碰到一個ClassNotFoundException的異常:

ClassLoader referenced unknown path: android.content.res.AssetManager@d75e3f9/out.jar

 java.lang.ClassNotFoundException: Didn't find class "com.example.dextest.ShowToast" on path: DexPathList[[],nativeLibraryDirectories=[/system/lib, /system/vendor/lib]]
     at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
     at com.example.test.MainActivity.loadDex(MainActivity.java:44)

因爲一開始我直接使用assets目錄下的jar包,好像加載不進來,然後我就使用FileUtils把jar包複製到另外一個目錄就可以了。
在測試機點擊按鈕就可以看到效果了:
在這裏插入圖片描述
總結一下就是使用DexClassLoader來獲取dex中的類,然後通過反射的方式運行類中的方法。

還有幾個問題:
a、如果原有的類有bug,現在要動態替換原有類,如何操作?
參考android熱修復,使用Javassist。
b、如果要動態加載新的activity,如何管理生命週期?
c、如何加載資源文件?

參考:類加載機制系列2——深入理解Android中的類加載器
Android動態加載dex入門
Android動態加載Dex過程

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