插件化緣來java
插件化技術最初源於免安裝運行APK的想法,這個免安裝的APK就能夠理解爲插件,而支持插件的APP咱們通常叫宿主。android
插件化解決的問題數組
插件化與組件化app
組件化:組件化開發就是將一個APP分紅多個模塊,每一個模塊都是一個組件,開發的過程當中咱們可讓這些組件相互依賴或者單獨調試部分組件等,可是最終發佈的時候是將這些組件合併統一成一個APK,這就是組件化開發。框架
插件化:插件化開發和組件化略有不一樣,插件化開發是將整個APP拆分紅多個模塊,這些模塊包括一個宿主和多個插件,每一個模塊都是一個APK,最終打包的時候宿主APK和插件APK分開打包。ide
經常使用插件化框架對比函數
特性 | dynamic-load-apk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
做者 | 任玉剛 | 攜程 | wequick | 360 | 滴滴 |
支持四大組件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
組件須要/不須要在宿主manifest中預註冊 | 不須要 | 須要 | 不須要 | 不須要 | 不須要 |
插件能夠/不能夠依賴宿主 | 能夠 | 能夠 | 能夠 | 不能夠 | 能夠 |
支持/不支持PendingIntent | 不支持 | 不支持 | 不支持 | 支持 | 支持 |
Android特性支持 | 大部分 | 大部分 | 大部分 | 幾乎所有 | 幾乎所有 |
兼容性適配 | 通常 | 通常 | 中等 | 高 | 高 |
插件構建 | 無 | 部署aapt | Gradle插件 | 無 | Gradle插件 |
咱們知道Java的類加載採用雙親委託的加載機制,有效防止類的重複加載和保護核心類的加載。Android的加載機制與Java的類加載機制相似,但也有區別。Android中的各個類加載器之間的關係以下圖所示。組件化
在Android中,在APP運行時,全部的應用自有的包含字節碼文件(.class)的dex
文件被包裝成一個 Element
對象,放在了一個Element[] elements
數組中,每個element
元素對應一個dex
文件,應用尋找特定某個類時,會從前日後依次遍歷該數組,直到找到或遍歷到尾。測試
本文講的簡單插件化實現就利用該Element數組實現:新建一個Element[] newElements
數組,在新建的newElements
數組中實現宿主的Element[] hostElements
數組和插件Element[] pluginElements
數組的拼接,並用新的Element
數組替換宿主原有的數組。ui
當系統經過PathClassLoader
去加載應用程序的dex文件中Java類時,PathClassLoader
並無重寫loadClass()
方法,因此接下來由PathClassLoader
的父類BaseDexClassLoader
嘗試執行加載任務,然而BaseDexClassLoader
也沒有重寫loadClass()
方法,則依次向上調用父類加載器的loadClass()
方法,父類加載器(由最頂級父類加載器開始嘗試loadClass
)在各自的加載範圍內嘗試加載須要的類,失敗以後依次向下調用子類加載器的findClass()
方法(也就是雙親委託機制那一套)。最終會調用到BaseDexClassLoader
的findClass()
方法。
// PathClassLoader 去掉註釋後的所有代碼,只有兩個構造函數
package dalvik.system;
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);
}
}
複製代碼
public class BaseDexClassLoader extends ClassLoader {
// ...
private final DexPathList pathList;
// ...
@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;
}
// ...
}
複製代碼
在該方法內,程序會調用BaseDexClassLoader
類中私有成員屬性DexPathList pathList
的findClass()
方法。
final class DexPathList {
// ...
private Element[] dexElements;
// ...
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;
}
// ...
}
複製代碼
在這段代碼中,用加強for
循環從前日後去遍歷dexElements
數組,該數組即爲上面圖片中的說起的Element[]
數組。
插件中的MainActivity.java未額外處理,以下:
package com.tongbo.plugin;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製代碼
Test類用於以後插件引入以後的測試,以下:
package com.tongbo.plugin;
import android.util.Log;
public class Test {
public static void print() {
Log.e("TB", "Test class from com.tongbo.plugin");
}
}
複製代碼
plugin\build\outputs\apk\debug
中找到plugin-debug.apk
,並上傳到模擬器的/sdcard/
路徑。在onCreate()
回調中,調用LoadUtil.loadClass(this)
實現插件的加載(詳見3.3.2);
在宿主的主頁面,添加一個按鈕,並在onClick
屬性綁定toStartPlugin(View view)
方法,並在方法中反射調用插件的類的方法Test.print()
;
package com.tongbo.pluginbasic;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LoadUtil.loadClass(this);
}
public void toStartPlugin(View view) {
Log.e("TB", "btn is clicked.");
try {
Class<?> clazz = Class.forName("com.tongbo.plugin.Test");
Method method = clazz.getMethod("print");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
LoadUtil
類中的loadClass(Context context)
方法是核心的插件載入方法,主要分爲如下幾個步驟:
dalvik.system.BaseDexClassLoader
的pathList
字段以及dalvik.system.DexPathList
的dexElements
字段並設置訪問權限;dexElements
字段的值;dexElements
字段的值;Array.newInstance()
方法建立新的Element[]
,在新建立的Element[]
中利用System.arraycopy()
方法完成數組的拼接;dexElements
字段的值爲新建的Element[]
,完成插件加載。package com.tongbo.pluginbasic;
import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class LoadUtil {
private final static String pluginApkPath = "/sdcard/plugin-debug.apk";
public static void loadClass(Context context) {
try {
//TODO:to get 'pathList' field and 'dexElements' field by reflection.
//private final DexPathList pathList;
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
//private Element[] dexElements;
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//TODO:to get the value of host's dexElements field.
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object hostPathList = pathListField.get(pathClassLoader);
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
//TODO:to get the value of plugin's dexElements field.
DexClassLoader dexClassLoader = new DexClassLoader(
pluginApkPath,
context.getCacheDir().getAbsolutePath(),
null,
context.getClassLoader()
);
Object pluginPathList = pathListField.get(dexClassLoader);
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
//TODO:to add host's dexElements and plugin's dexElements together in a newly created Element array.
Object[] dexElements = (Object[]) Array.newInstance(
pluginDexElements.getClass().getComponentType(),
hostDexElements.length + pluginDexElements.length
);
System.arraycopy(
hostDexElements,
0, dexElements,
0,
hostDexElements.length
);
System.arraycopy(
pluginDexElements,
0,
dexElements,
hostDexElements.length,
pluginDexElements.length
);
//TODO:to set the newly created Element array to the host's dexElements field and done.
dexElementsField.set(hostPathList, dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
將宿主APP運行起來,點擊按鈕,控制檯將打印以下(成功):