https://blog.csdn.net/lin20044140410/article/details/104584877
什麼是插件化?
直白點說,就是去運行沒有安裝的apk,把這些沒有安裝過的apk理解爲插件,把運行這些插件的apk稱爲宿主.
爲什麼需要插件化?
由於宿主可以在運行時動態去加載和運行插件,這就可以把apk中不常用的功能模塊做成插件,減小了apk包的大小,實現了apk功能的動態擴展.
插件化和組件化的區別:
組件化,是把app分成多個模塊,獨立開發,根據需求,利用gradle配置模塊間的依賴關係,但是最終發佈時模塊和app會打包成一個apk.
插件化,也是把app分成多個模塊,這些模塊中有一個宿主,多個插件,最終打包時宿主apk和插件apk可以分開打包,插件apk也可以在需要時由網絡下載,然後加載運行.
插件化實現的思路:
1)加載插件的類
2)加載插件的資源
3)啓動插件中的組件(四大組件)
這篇文章先來實現第一個問題:加載插件的類,這裏插件的類,是普通的類,不是四大組件,四大組件的加載運行會放在後面的文章來討論.
加載類就要用到類加載器,java中jvm加載的class文件,android中的虛擬機加載的dex文件(就是多個class文件的合併),這裏主要看android中如何加載dex文件的.
android中的類加載分兩種:系統類加載器,自定義的類加載器,他們的關係如圖:
BootClassLoader 用於加載framework層的class文件
PathClassLoader 用於加載應用程序中class文件,這裏也包括應用中依賴的庫文件,具體形式可以是apk,jar,zip中的dex文件.
DexClassLoader 用於加載制定的dex文件.
在一個自動生成的app中,MainActivity.java 這個類是由PathClassLoader來加載的, 而android.app.Activity.java這個framework中類時由BootClassLoader加載的.
從源碼看,DexClassLoader,PathClassLoader除了構造函數外,沒有實現任何代碼,類加載的工作都是由父類BaseDexClassLoader時完成的.
這裏有一點需要注意的是:PathCalssLoader的parent類加載器是BootClassLoader,並不是其繼承關係的父類.
在分析類加載流程之前,先看下類的生命週期:
加載 是 類加載過程中的一個階段,在加載階段,虛擬機要完成3件事情:
1)通過類的全限定名,獲取類的二進制字節流
2)將這個字節流代表的靜態存儲結構,轉化爲方法區的運行時數據結構
3)在內存中生成一個代表這個類的java.lang.class對象,作爲方法區這個類的各種數據的訪問入口,這個Class對象是比較特殊的,雖然它是對象,卻是放在方法區的.
下面就通過源碼看下加載一個類的過程,從loadClass方法的實現看起,DexClassLoader,PathClassLoader及父類BaseDexClassLoader,一直往上查找,這個方法的實現在ClassLoader.java中.
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
這裏的實現就是雙親委派模型,
1,首先去查找這個類是不是已經加載過,是就直接返回,否則,
2, 遞歸的去請求parent加載器去完成加載,在遞歸過程中,走到BootClassLoader後,就不會再請求其parent來加載,而是通過其findClass來加載,如果無法完成這個加載請求,就返回null.
3,如果都沒有加載成功,才由自己來完成加載findClass,
這個雙親委託機制,讓類的加載有了一種優先級的層次關係,比如我們Activity這個類,不管是誰請求加載這個類,最終都是委託給這個模型最頂端的BootClassLoader來加載,所以activity在各種類加載環境中都是同一個類,如果沒有這種機制,自己去定義類加載器,然後自己定義一個android.app.Activity類,那麼系統中就會出現多個不同的Activity類,那就亂了.
從上面雙親委派模型的加載流程,如果要自己加載類,時調用findClass方法,下面看BaseDexClassLoader.java中的這個方法的實現:
(因爲DexClassLoader,PathClassLoader都沒定義這個方法,所以實際調用的是BaseDexClassLoader.java中的)
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
// TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
它又是通過DexPathList來實現的.
DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
dexElements是一個數組,其中的元素 Element裏面包含了dex文件,如果app中有多個dex文件,這個數組就會有多個Element元素,每個Element元素中包含一個dex文件,
DexPathList#Element.java
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
最終是通過Element中的dexfile來獲取類的二進制字節流.
在回頭去看看dexElements這個數組怎麼來的?
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
從構造函數的回溯,可以看到dexElements中元素,來自與第一個參數dexpath,這個路徑通常就是apk的路徑,或者jar,zip,dex文件的路徑,具體的值是有創建類加載器傳入的參數確定.
分析到這裏,應該能明白一點:如果要讓app能加載插件的class文件,只要把插件的dex文件,也存入dexElement數組就OK 了.
通過代碼驗證,使用DexClassLoader,PathClassLoader能不能成功加載插件的類.
先定義一個插件module,新建測試類:
package com.test.plugintest;
import android.util.Log;
public class PlugInDemo {
public static void testPlugIn() {
Log.d("PlugIn","from plugin PlugInDemo.");
}
}
把這個類打包成一個dex文件,具體做法是使用sdk下dx工具,把class文件打包成dex,
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
dx --dex --output=plugIn.dex com\test\plugintest\PlugInDemo.class
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
執行上面的命令,會在當前目錄下生成一個plugIn.dex文件,這裏有一點值得注意,執行dx命令的位置最好在classes這個目錄,然後指定具體class文件時,使用全類名
把plugIn.dex拷貝到sdcard目錄下,
public void loadClassPlugIn() {
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
MainActivity.this.getCacheDir().getAbsolutePath(),
null,
MainActivity.this.getClassLoader());
// PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
// MainActivity.this.getClassLoader());
try {
Class<?> clazz = dexClassLoader.loadClass("com.test.plugintest.PlugInDemo");
Method method = clazz.getMethod("testPlugIn");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
在宿主App的MainActivity中,可以通過上面的ClassLoader加載PlugInDemo.java類,通過反射調用其中方法。
通過這個測試,只是去驗證通過自定義類加載器可以去加載執行一個插件中的類.但是這個插件的類並沒有合併到app中.
下面就來實現把插件的類,合併到宿主app中,實現步驟:
1,創建插件的DexClassLoader類加載器,然後反射拿到插件的dexElements數組
2,獲取宿主的PathClassLoader類加載器,然後反射拿到宿主的dexElements數組
3,把宿主和插件的兩個dexElements數組合並
DexPathList.java
private Element[] dexElements;
BaseDexClassLoader.java
private final DexPathList pathList;
dexElements在DexPathList中,而DexPathList在BaseDexClassLoader中,所以先獲取類加載器,然後反射獲取到DexPathList,在反射拿到dexElements.
public void loadPlugInModule() {
try {
//BastDexClassLoader,DexPathList,宿主和插件可公用
Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
pathListField.setAccessible(true);
Class<?> dexPathListClassLoader = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClassLoader.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//創建插件的DexClassLoader類加載器,然後反射拿到插件的dexElements數組
DexClassLoader plugInDexClassLoader = new DexClassLoader(
"/sdcard/plugintest-debug.apk",
mContext.getCacheDir().getAbsolutePath(),
null,
mContext.getClassLoader());
//獲取插件類加載器中DexPathList實例對象
Object plugInDexPathList = pathListField.get(plugInDexClassLoader);
//拿到插件的dexElements數組
Object[] plugInDexElements = (Object[]) dexElementsField.get(plugInDexPathList);
//獲取宿主的PathClassLoader類加載器,然後反射拿到宿主的dexElements數組
PathClassLoader hostPathClassLoader = (PathClassLoader) mContext.getClassLoader();
//獲取宿主類加載器中DexPathList實例對象
Object hostDexPathList = pathListField.get(hostPathClassLoader);
//拿到宿主的dexElements數組
Object[] hostDexElements = (Object[]) dexElementsField.get(hostDexPathList);
//創建新的DexElements數組
Object[] dexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
hostDexElements.length + plugInDexElements.length);
//合併宿主和插件的dexElements數組
System.arraycopy(hostDexElements,0, dexElements, 0, hostDexElements.length);
System.arraycopy(plugInDexElements, 0, dexElements, hostDexElements.length, plugInDexElements.length);
//用合併後的數組替換宿主的原dexElements數組
dexElementsField.set(hostDexPathList, dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
在將插件apk中class加載後,就可以通過反射訪問其中的class屬性了.
public void loadClassPlugIn() {
// DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
// MainActivity.this.getCacheDir().getAbsolutePath(),
// null,
// MainActivity.this.getClassLoader());
//// PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
//// MainActivity.this.getClassLoader());
try {
Class<?> clazz = Class.forName("com.test.plugintest.PlugInDemo");
Method method = clazz.getMethod("testPlugIn");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}