ClassLoader類載入器(一):動態代理模式
一. 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方法,最後看結果
確實是有列印的,這樣我們就相當於能正常跑完一個動態載入類的流程。但是這個地方還是有些問題,執行之後我看了下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了,這個問題以後碰到再說吧,暫時注意一下就行。