作者:Pan Geng
什麼是插件化
概念
插件化技術最初源於免安裝運行 apk 的想法,這個免安裝的 apk 就可以理解爲插件,而支持插件的 app 我們一般叫宿主。宿主可以在運行時加載和運行插件,這樣便可以將 app 中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實現 app 功能的動態擴展。
我們知道計算機主板就是由一系列的插槽組成的,我們需要什麼功能,給它插上對應的芯片或顯卡就可以了,從而實現熱拔插。基於這個原理,軟件方面的熱拔插就是插件化
插件化解決的問題
- APP的功能模塊越來越多,體積越來越大,這樣可以將一些業務模塊做成插件化,按需加載,從而減小安裝包的體積
- 模塊之間的耦合度高,協同開發溝通成本越來越大
- 方法數目可能超過65535,APP佔用的內存過大
- 應用之間的互相調用
組件化與插件化的區別
組件化開發就是將一個app分成多個模塊,每個模塊都是一個組件,開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發佈的時候是將這些組件合併統一成一個apk,這就是組件化開發。
插件化開發和組件化略有不同,插件化開發是將整個app拆分成多個模塊,這些模塊包括一個宿主和多個插件,每個模塊都是一個apk,最終打包的時候宿主apk和插件apk分開打包。
各插件化框架對比
市面上比較流行的插件化框架也有很多,他們之間都有哪些區別呢?
我們在選擇開源框架的時候,需要根據自身的需求來,如果加載的插件不需要和宿主有任何耦合,也無須和宿主進行通信,比如加載第三方 App,那麼推薦使用 RePlugin,其他的情況推薦使用 VirtualApk。
插件化實現
插件apk是沒有安裝的,那麼怎麼讓宿主去加載它呢?我們知道,一個apk是由代碼和資源組成的,所以需要考慮以下問題:
- 如何加載插件中的類?
- 如何加載插件中的資源?
- 當然還有最重要的一個問題,四大組件如何調用呢?四大組件是需要註冊的,而插件apk中的組件顯然不會在宿主提前註冊,那麼如何去調用它呢?
下面我們就來一步一步解決這些問題
ClassLoader類加載器
以前在講熱修復的時候,我簡單地介紹了一下ClassLoader的加載機制。java源碼文件在編譯後會生成一個class文件,而在Android中,將代碼編譯後會生成一個 apk 文件,將 apk 文件解壓後就可以看到其中有一個或多個 classes.dex 文件,它就是安卓把所有 class 文件進行合併,優化後生成的。
ClassLoader的加載機制:
java 中 JVM 加載的是 class 文件,而安卓中 DVM 和 ART 加載的是 dex 文件,雖然二者都是用的 ClassLoader 加載的,但因爲加載的文件類型不同,還是有些區別的,所以接下來我們主要介紹安卓的 ClassLoader 是如何加載dex 文件的。
ClassLoader實現類
在Android中,ClassLoader是一個抽象類,它的實現類主要分爲兩種類型:系統類加載器(BootClassLoader),和自定義類加載器(PathClassLoader | DexClassLoader)
先看一下ClassLoader加載流程圖:
- BootClassLoader
用於加載Android Framework層的class文件,比如 Activity、Fragment,不過需要注意的是AppCompatActivity雖然也是google工程師提供的類,但是一個第三方包中的類,並不輸入Framwork層,所以AppCompatActivity並不是使用BootClassLoader加載的
- PathClassLoader
用於Android應用程序類加載器。可以加載指定的dex, 以及jar、zip、apk中的classes.dex
- DexClassLoader
在Android8.0以後的API中,和 PathClassLoader是沒有任何區別的,而在以前的API中,兩者只有一個設置加載路徑的區別(有的文章說,PathClassLoader只支持直接操作dex格式文件,而DexClassLoader可以支持.apk、.jar和.dex文件,並且會在指定的outpath路徑釋放出dex文件。其實不然,甚至可以說兩者沒有任何區別)
先放一張ClassLoader類繼承關係圖,相信都能看懂,就不多講了,下面來看一下PathClassLoader 和 DexClassLoader的源碼:
// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
// optimizedDirectory 直接爲 null
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// optimizedDirectory 直接爲 null
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// API 小於等於 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
// 26開始,super裏面改變了,看下面兩個構造方法
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// DexPathList 的第四個參數是 optimizedDirectory,可以看到這兒爲 null
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
根據源碼就可以瞭解到,PathClassLoader 和 DexClassLoader 都是繼承自 BaseDexClassLoader,且類中只有構造方法,它們的類加載邏輯完全寫在 BaseDexClassLoader 中。
其中我們值得注意的是,在8.0之前,它們二者的唯一區別是第二個參數 optimizedDirectory,這個參數的意思是生成的 odex(優化的dex)存放的路徑,PathClassLoader 直接爲null,而 DexClassLoader 是使用用戶傳進來的路徑,而在8.0之後,二者就完全一樣了。
下面我們再來了解下 BootClassLoader 和 PathClassLoader 之間的關係:
// 在 onCreate 中執行下面代碼
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("leo", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());
打印結果:
classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file
"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file
"/data/app/com.enjoy.pluginactivity-T4YwTh-
8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-
T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]
classLoader:java.lang.BootClassLoader@a26e88d
classLoader:java.lang.BootClassLoader@a26e88d
通過打印結果可知,應用程序類是由 PathClassLoader 加載的,Activity 類是 BootClassLoader 加載的,並且BootClassLoader 是 PathClassLoader 的 parent,這裏要注意 parent 與父類的區別。這個打印結果我們下面還會提到。
加載原理
那麼如何使用類加載器去從dex中加載一個插件類呢?很簡單
比如,有一個apk文件,路徑是apkPath,裏面有個類com.plugin.Test,就可以通過反射加載一個類:
// 初始化一個類加載器
DexClassLoader classLoader = new DexClassLoader(dexPath, context.getCacheDir().getAbsolutePath, null, context.getClassLoader);
// 獲取插件中的類
Class<?> clazz = classLoader.loadClass("com.plugin.Test");
// 調用類中的方法
Method method = clazz.getMethod("test", Context.class)
method.invoke(clazz.newInstance(), this)
dex中加載類很簡單,但是我們需要的是將插件中的dex加載到宿主裏面,又該怎麼做呢?其實原理還是跟熱修復一樣,下面就以API 26 Android 8.0舉例,通過源碼,看一下DexClassLoader類加載器是怎麼加載一個apk中的dex文件的。
通過查找發現,DexClassLoader並沒有加載類的方法,繼續看它的父類,最後在ClassLoader類中找到了一個loadClass方法,看來就是通過這個方法來加載類了:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1. 檢測這個類是否已經被加載,如果已經被加載了就可以直接返回了
Class<?> c = findLoadedClass(name);
// 如果類未被加載
if (c == null) {
try {
// 2. 判斷是否有上級加載器,使用上級加載器的loadClass方法去加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 正常情況下是不會走到這裏的,因爲最終ClassLoader都會走到BootClassLoader,重寫了loadClass方法結束掉了遞歸
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 3. 如果所有的上級都沒找到,就調用findClass方法去查找
if (c == null) {
c = findClass(name);
}
}
return c;
}
上面類加載分爲了3個步驟
1、 檢測這個類是否已經被加載,最終會調用到native方法實現查找,這裏就不深入了:
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
//native方法
return VMClassLoader.findLoadedClass(loader, name);
}
2、如果沒被找到,就會從parent中調用loadClass方法去查找,依次遞歸,如果找到了就返回,如果所有的上級都沒有找到,又會調用到findClass一級一級的去查找。這個過程就是雙親委託機制
3、 findClass
// -->2 加載器一般都會重寫這個方法,定義自己的加載規則
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
根據前面的打印結果我們可以看懂,ClassLoader的最上級是BootClassLoader,來 看下它是如何重寫的loadClass方法,結束遞歸的:
class BootClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return Class.classForName(name, false, null);
}
@Override
protected Class<?> loadClass(String className, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
}
從上面可以看到 BootClassLoader 重寫了 findClass 和 loadClass 方法,並且在 loadClass 方法中,不再獲取 parent,從而結束了遞歸。
接着往下走,如果所有的parent都沒找到,DexClassLoader是如何加載的,通過查找,其實現方法在它的父類BaseDexClassLoader中:
// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在 pathList 中查找指定的 Class
Class c = pathList.findClass(name, suppressedExceptions);
return c;
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// 初始化 pathList
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
findClass中有調用了DexPathList中的findClass方法,繼續:
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//通過 Element 獲取 Class 對象
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
return null;
}
到這裏一目瞭然,class對象就是從Element中獲得的,而每一個Element就對應了一個dex文件,因爲一個apk中dex文件可能有多個,所以就使用了數組來盛放Element。到這裏加載apk中的類大家是不是就有思路了?
- 創建插件的ClassLoader加載器(PathClassLoader或DexClassLoader),然後通過反射,獲取插件的dexElements數組的值。
- 獲取宿主的ClassLoader加載器,通過反射獲取宿主的dexElements數組的值。
- 合併宿主和插件的dexElements數組,生成一個新的數組
- 通過反射將新的數組重新賦值給宿主的dexElements
實現方法
廢話不多說,直接上代碼:(我這裏使用了kotlin,寫起來感覺方便一些)
fun load(context: Context) {
// 獲取 pathList
val systemClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")
val pathListField = systemClassLoader.getDeclaredField("pathList")
pathListField.isAccessible = true
// 獲取 dexElements
val dexPathListClass = Class.forName("dalvik.system.DexPathList")
val dexElementsField = dexPathListClass.getDeclaredField("dexElements")
dexElementsField.isAccessible = true
// 獲取宿主的Elements
val hostClassLoader = context.classLoader
val hostPathList = pathListField.get(hostClassLoader)
val hostElements = dexElementsField.get(hostPathList) as kotlin.Array<Any>
// 獲取插件的Elements
val pluginClassLoader = PathClassLoader("sdcard/plugin-debug.apk", context.classLoader)
val pluginPathList = pathListField.get(pluginClassLoader)
val pluginElements = dexElementsField.get(pluginPathList) as kotlin.Array<Any>
// 創建數組
val newElements =
Array.newInstance(
pluginElements.javaClass.componentType!!,
hostElements.size + pluginElements.size
) as kotlin.Array<Any>
// 給新數組賦值
// 先用宿主的,再用插件的
System.arraycopy(hostElements, 0, newElements, 0, hostElements.size)
System.arraycopy(pluginElements, 0, newElements, hostElements.size, pluginElements.size)
// 將生成的新值賦給 "dexElements" 屬性
dexElementsField.set(hostPathList, newElements)
}
這樣就合併了兩個dex文件的類,宿主中就可以直接加載插件中的類了
private fun loadApk() {
try {
val clazz = Class.forName("com.kangf.plugin.Test")
val method = clazz.getMethod("test", Context::class.java)
method.invoke(clazz.newInstance(), this)
} catch (e: Exception) {
e.printStackTrace()
// 調用上面的load方法
Toast.makeText(this, "請先點擊加載apk", Toast.LENGTH_LONG).show()
}
}
好了,下面來看一下運行效果吧!
篇幅限制,今天就講到這裏,還有兩個問題(加載資源圖片和四大組件)看以下兩篇文章。
從零開始實現一個插件化框架(二):
從零開始實現一個插件化框架(三):
本文源碼地址:
最後
另外還分享一份大佬收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記,高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料
這些都是我現在閒暇還會反覆翻閱的精品資料。裏面對近幾年的大廠面試高頻知識點都有詳細的講解。相信可以有效的幫助大家掌握知識、理解原理。
當然你也可以拿去查漏補缺,提升自身的競爭力。
如果你有需要的話,可以 點這領取
喜歡本文的話,不妨順手給我點個贊、評論區留言或者轉發支持一下唄~