1. 程式人生 > 其它 >ClassLoader類載入器(一):動態代理模式

ClassLoader類載入器(一):動態代理模式

技術標籤:Androidjavaandroid動態代理

一. java的ClassLoader

1.classloader型別

先了解下java的ClassLoader,因為android的ClassLoader會有些不同。
java預設提供三種ClassLoader
(1)Bootstrp ClassLoader
(2)ExtClassLoader
(3)AppClassLoader
先記住有這三種就行,至於有什麼用,就先不講了,因為我們用到的是android的ClassLoader。還有注意的是Bootstrp ClassLoader是底層用C++寫的,它們的關係是AppClassLoader繼承ExtClassLoader,ExtClassLoader繼承Bootstrp ClassLoader。

2.classloader工作流程

java的classloader有個雙親委派模式,網上有很多,看別人的圖就能比較好懂,我這邊就不盜圖了。簡單來說就是當需要載入一個類的時候,先去看AppClassLoader有沒有載入過,沒有再看ExtClassLoader有沒有載入過,沒有再看Bootstrp ClassLoader有沒有載入過,沒有再從Bootstrp ClassLoader中找是不是他那邊的類,不是再向下交給ExtClassLoader處理。
雙親委派這個要記住。然後雙親委派的作用主要是能讓類不重複載入和保證安全(比如你無法自定義系統類)。

二. android的ClassLoader

大概瞭解java的ClassLoader之後我們也來簡單瞭解下Android的ClassLoader。

1.classloader型別

android的classloader就比較多了,沒用到的後面再講,這裡我們要做動態載入,主要也是用到3個classloader
(1)ClassLoader 這是頂層的classloader類
(2)BaseDexClassLoader 這個類主要做類載入的操作
(3)PathClassLoader 這個是BaseDexClassLoader 的子類
(4)DexClassLoader 這個是BaseDexClassLoader 的子類
然後這樣,BaseDexClassLoader 是做具體的操作,我打算放到後面的篇章再詳細說,所以要做動態載入主要看PathClassLoader和DexClassLoader兩個類。

說得簡單一點,這兩個類的最好區分就在構造方法。
(1)先看DexClassLoader的構造方法

public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

dexPath是Dex的路徑,optimizedDirectory指的是優化之後的目錄,有個說法是裝生成的odex檔案。librarySearchPath指要使用的C/C++程式碼,parent指父載入器。
(2)再看PathClassLoader的構造方法

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

看得出PathClassLoader只有三個構造方法,對比DexClassLoader,少了optimizedDirectory。
所以說dalvik虛擬機器上PathClassLoader只能載入已安裝apk的dex。而我們要實現動態載入,所以使用的類載入器是DexClassLoader

2.classloader工作流程

和java一樣,也是使用雙親委派

三. 寫個動態載入的Demo

我們先搞個簡單的,不含任何資源,就僅僅呼叫Java程式碼,確認是否能夠跑一遍流程。

1.開發外掛
public class TestLog {

    public void say(String str){
        Log.v("mmp","外掛列印:"+str);
    }

}

我就加了這個一個類,然後打包成apk

2.宿主動態載入
    private void loadPlugin(){
        try {
            String pluginPath = getExternalFilesDir("plugin").getPath() + "/TestPlugin.apk" ;
            String targetPath = getExternalFilesDir("target").getPath();
            DexClassLoader dexClassLoader = new DexClassLoader(pluginPath,targetPath,null,getClassLoader());
            Class<?> cls = dexClassLoader.loadClass("com.example.kylin.testapp.TestLog");
            cls.getDeclaredMethod("say",String.class).invoke(cls.newInstance(),"AAAAAAAAAAAAA");
        } catch (Exception e) {
            e.printStackTrace();
            Log.v("mmp","錯誤:"+e.toString());
        }
    }

簡單就這樣寫,我們把外掛打出的apk命名為TestPlugin.apk,然後放到plugin資料夾下,呼叫DexClassLoader 去獲取到Class,再用反射呼叫Class的say方法,最後看結果

image

確實是有列印的,這樣我們就相當於能正常跑完一個動態載入類的流程。但是這個地方還是有些問題,執行之後我看了下target資料夾是沒東西的,而odex檔案是在plugin資料夾下生成了一個oat資料夾裡面,這就比較奇怪,我先看看什麼原因再補上。


四. (補充)載入資源

除了用ClassLoader載入程式碼,比較核心的操作還有資源和生命週期,生命週期這個涉及的水有點深,我這邊就打算把載入資源也放在一起寫算了,不打算再分一章了。

要載入外掛的資源,要用到的類是AssetManager,當建立Resources的時候第一個引數就是傳AssetManager。

這裡我寫個Demo獲取外掛apk中的一張圖片,然後用一個ImageView來展示。

        // 載入圖片
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            int path = (int) AssetManager.class.getDeclaredMethod("addAssetPath",String.class).invoke(assetManager, pluginPath);
            Resources resources = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());
            Drawable drawable = resources.getDrawable(resources.getIdentifier("test", "drawable","com.example.plugin.testapp"));
        }catch (Exception e){
            Log.v("mmp","載入圖片失敗"+e.toString());
        }

resources.getIdentifier就相當於R.,這個沒啥好說的,要注意的是第三個引數傳的是外掛的包名。

比較需要注意的是addAssetPath這個方法,這個是AssetManager 根據路徑獲取資源的方法,不過是一個隱藏方法(@hide)所以需要用反射來呼叫,但是android9.0之後谷歌打算禁用 hide APIs了,這個問題以後碰到再說吧,暫時注意一下就行。