什么是插件化
概念
插件化技術(shù)最初源于免安裝運(yùn)行 apk 的想法,這個(gè)免安裝的 apk 就可以理解為插件,而支持插件的 app 我們一般叫宿主。宿主可以在運(yùn)行時(shí)加載和運(yùn)行插件,這樣便可以將 app 中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實(shí)現(xiàn) app 功能的動(dòng)態(tài)擴(kuò)展。
我們知道計(jì)算機(jī)主板就是由一系列的插槽組成的,我們需要什么功能,給它插上對(duì)應(yīng)的芯片或顯卡就可以了,從而實(shí)現(xiàn)熱拔插?;谶@個(gè)原理,軟件方面的熱拔插就是插件化
插件化解決的問題
- APP的功能模塊越來(lái)越多,體積越來(lái)越大,這樣可以將一些業(yè)務(wù)模塊做成插件化,按需加載,從而減小安裝包的體積
- 模塊之間的耦合度高,協(xié)同開發(fā)溝通成本越來(lái)越大
- 方法數(shù)目可能超過(guò)65535,APP占用的內(nèi)存過(guò)大
- 應(yīng)用之間的互相調(diào)用
組件化與插件化的區(qū)別
組件化開發(fā)就是將一個(gè)app分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件,開發(fā)的過(guò)程中我們可以讓這些組件相互依賴或者單獨(dú)調(diào)試部分組件等,但是最終發(fā)布的時(shí)候是將這些組件合并統(tǒng)一成一個(gè)apk,這就是組件化開發(fā)。
插件化開發(fā)和組件化略有不同,插件化開發(fā)是將整個(gè)app拆分成多個(gè)模塊,這些模塊包括一個(gè)宿主和多個(gè)插件,每個(gè)模塊都是一個(gè)apk,最終打包的時(shí)候宿主apk和插件apk分開打包。
各插件化框架對(duì)比
市面上比較流行的插件化框架也有很多,他們之間都有哪些區(qū)別呢?
| 特性 | dynamic-load-apk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
|---|---|---|---|---|---|
| 作者 | 任玉剛 | 攜程 | wequick | 360 | 滴滴 |
| 支持四大組件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
| 組件無(wú)需在宿主manifest中預(yù)注冊(cè) | √ | × | √ | √ | √ |
| 插件可以依賴宿主 | √ | √ | √ | × | √ |
| 支持PendingIntent | × | × | × | √ | √ |
| Android特性支持 | 大部分 | 大部分 | 大部分 | 幾乎全部 | 幾乎全部 |
| 兼容性適配 | 一般 | 一般 | 中等 | 高 | 高 |
| 插件構(gòu)建 | 無(wú) | 部署aapt | Gradle插件 | 無(wú) | Gradle插件 |
我們?cè)谶x擇開源框架的時(shí)候,需要根據(jù)自身的需求來(lái),如果加載的插件不需要和宿主有任何耦合,也無(wú)須和宿主進(jìn)
行通信,比如加載第三方 App,那么推薦使用 RePlugin,其他的情況推薦使用 VirtualApk。
插件化實(shí)現(xiàn)
插件apk是沒有安裝的,那么怎么讓宿主去加載它呢?我們知道,一個(gè)apk是有代碼和資源組成的,所以只需要考慮兩個(gè)問題即可:
如何加載插件中的類?
如何加載插件中的資源?
當(dāng)然還有最重要的一個(gè)問題,四大組件如何調(diào)用呢?四大組件是需要注冊(cè)的,而插件apk中的組件顯然不會(huì)在宿主提前注冊(cè),那么如何去調(diào)用它呢?
下面我們就來(lái)一步一步解決這些問題
ClassLoader類加載器
以前在講熱修復(fù)的時(shí)候,我簡(jiǎn)單地介紹了一下ClassLoader的加載機(jī)制。java源碼文件在編譯后會(huì)生成一個(gè)class文件,而在Android中,將代碼編譯后會(huì)生成一個(gè) apk 文件,將 apk 文件解壓后就可以看到其中有一個(gè)或多個(gè) classes.dex 文件,它就是安卓把所有 class 文件進(jìn)行合并,優(yōu)化后生成的。
java 中 JVM 加載的是 class 文件,而安卓中 DVM 和 ART 加載的是 dex 文件,雖然二者都是用的 ClassLoader 加
載的,但因?yàn)榧虞d的文件類型不同,還是有些區(qū)別的,所以接下來(lái)我們主要介紹安卓的 ClassLoader 是如何加載
dex 文件的。
ClassLoader實(shí)現(xiàn)類
在Android中,ClassLoader是一個(gè)抽象類,它的實(shí)現(xiàn)類主要分為兩種類型:系統(tǒng)類加載器(BootClassLoader),和自定義類加載器(PathClassLoader | DexClassLoader)
先看一下ClassLoader加載流程圖:
-
BootClassLoader
用于加載Android Framework層的class文件,比如 Activity、Fragment,不過(guò)需要注意的是AppCompatActivity雖然也是google工程師提供的類,但是一個(gè)第三方包中的類,并不輸入Framwork層,所以AppCompatActivity并不是使用BootClassLoader加載的
-
PathClassLoader
用于Android應(yīng)用程序類加載器??梢约虞d指定的dex, 以及jar、zip、apk中的classes.dex
-
DexClassLoader
在Android8.0以后的API中,和 PathClassLoader是沒有任何區(qū)別的,而在以前的API中,兩者只有一個(gè)設(shè)置加載路徑的區(qū)別(有的文章說(shuō),PathClassLoader只支持直接操作dex格式文件,而DexClassLoader可以支持.apk、.jar和.dex文件,并且會(huì)在指定的outpath路徑釋放出dex文件。其實(shí)不然,甚至可以說(shuō)兩者沒有任何區(qū)別)
先放一張ClassLoader類繼承關(guān)系圖,相信都能看懂,就不多講了,下面來(lái)看一下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里面改變了,看下面兩個(gè)構(gòu)造方法
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 的第四個(gè)參數(shù)是 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);
}
根據(jù)源碼就可以了解到,PathClassLoader 和 DexClassLoader 都是繼承自 BaseDexClassLoader,且類中只有構(gòu)造方法,它們的類加載邏輯完全寫在 BaseDexClassLoader 中。
其中我們值的注意的是,在8.0之前,它們二者的唯一區(qū)別是第二個(gè)參數(shù) optimizedDirectory,這個(gè)參數(shù)的意思是
生成的 odex(優(yōu)化的dex)存放的路徑,PathClassLoader 直接為null,而 DexClassLoader 是使用用戶傳進(jìn)來(lái)的
路徑,而在8.0之后,二者就完全一樣了。
下面我們?cè)賮?lái)了解下 BootClassLoader 和 PathClassLoader 之間的關(guān)系:// 在 onCreate 中執(zhí)行下面代碼
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("leo", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());
打印結(jié)果:
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
通過(guò)打印結(jié)果可知,應(yīng)用程序類是由 PathClassLoader 加載的,Activity 類是 BootClassLoader 加載的,并且
BootClassLoader 是 PathClassLoader 的 parent,這里要注意 parent 與父類的區(qū)別。這個(gè)打印結(jié)果我們下面還
會(huì)提到。
加載原理
那么如何使用類加載器去從dex中加載一個(gè)插件類呢?很簡(jiǎn)單
比如,有一個(gè)apk文件,路徑是apkPath,里面有個(gè)類com.plugin.Test,就可以通過(guò)反射加載一個(gè)類:
// 初始化一個(gè)類加載器
DexClassLoader classLoader = new DexClassLoader(dexPath, context.getCacheDir().getAbsolutePath, null, context.getClassLoader);
// 獲取插件中的類
Class<?> clazz = classLoader.loadClass("com.plugin.Test");
// 調(diào)用類中的方法
Method method = clazz.getMethod("test", Context.class)
method.invoke(clazz.newInstance(), this)
dex中加載類很簡(jiǎn)單,但是我們需要的是將插件中的dex加載到宿主里面,又該怎么做呢?其實(shí)原理還是跟熱修復(fù)一樣,下面就以API 26 Android 8.0舉例,通過(guò)源碼,看一下DexClassLoader類加載器是怎么加載一個(gè)apk中的dex文件的。
通過(guò)查找發(fā)現(xiàn),DexClassLoader并沒有加載類的方法,繼續(xù)看它的父類,最后在ClassLoader類中找到了一個(gè)loadClass方法,看來(lái)就是通過(guò)這個(gè)方法來(lái)加載類了:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1. 檢測(cè)這個(gè)類是否已經(jīng)被加載,如果已經(jīng)被加載了就可以直接返回了
Class<?> c = findLoadedClass(name);
// 如果類未被加載
if (c == null) {
try {
// 2. 判斷是否有上級(jí)加載器,使用上級(jí)加載器的loadClass方法去加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 正常情況下是不會(huì)走到這里的,因?yàn)樽罱KClassLoader都會(huì)走到BootClassLoader,重寫了loadClass方法結(jié)束掉了遞歸
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 3. 如果所有的上級(jí)都沒找到,就調(diào)用findClass方法去查找
if (c == null) {
c = findClass(name);
}
}
return c;
}
上面類加載分為了3個(gè)步驟
1、 檢測(cè)這個(gè)類是否已經(jīng)被加載,最終會(huì)調(diào)用到native方法實(shí)現(xiàn)查找,這里就不深入了:
protected final Class<?> findLoadedClass(String name) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
//native方法
return VMClassLoader.findLoadedClass(loader, name);
}
2、如果沒被找到,就會(huì)從parent中調(diào)用loadClass方法去查找,依次遞歸,如果找到了就返回,如果所有的上級(jí)都沒有找到,又會(huì)調(diào)用到findClass一級(jí)一級(jí)的去查找。這個(gè)過(guò)程就是雙親委托機(jī)制
3、 findClass
// -->2 加載器一般都會(huì)重寫這個(gè)方法,定義自己的加載規(guī)則
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
根據(jù)前面的打印結(jié)果我們可以看懂,ClassLoader的最上級(jí)是BootClassLoader,來(lái) 看下它是如何重寫的loadClass方法,結(jié)束遞歸的:
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,從而結(jié)束了遞歸。
接著往下走,如果所有的parent都沒找到,DexClassLoader是如何加載的,通過(guò)查找,其實(shí)現(xiàn)方法在它的父類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中有調(diào)用了DexPathList中的findClass方法,繼續(xù):
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
//通過(guò) Element 獲取 Class 對(duì)象
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
return null;
}
到這里一目了然,class對(duì)象就是從Element中獲得的,而每一個(gè)Element就對(duì)應(yīng)了一個(gè)dex文件,因?yàn)橐粋€(gè)apk中dex文件可能有多個(gè),所以就使用了數(shù)組來(lái)盛放Element。到這里加載apk中的類大家是不是就有思路了?
- 創(chuàng)建插件的ClassLoader加載器(PathClassLoader或DexClassLoader),然后通過(guò)反射,獲取插件的dexElements數(shù)組的值
- 獲取宿主的ClassLoader加載器,通過(guò)反射獲取宿主的dexElements數(shù)組的值。
- 合并宿主和插件的dexElements數(shù)組,生成一個(gè)新的數(shù)組
- 通過(guò)反射將新的數(shù)組重新賦值給宿主的dexElements
實(shí)現(xiàn)方法
廢話不多說(shuō),直接上代碼:(我這里使用了kotlin,寫起來(lái)感覺方便一些)
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>
// 創(chuàng)建數(shù)組
val newElements =
Array.newInstance(
pluginElements.javaClass.componentType!!,
hostElements.size + pluginElements.size
) as kotlin.Array<Any>
// 給新數(shù)組賦值
// 先用宿主的,再用插件的
System.arraycopy(hostElements, 0, newElements, 0, hostElements.size)
System.arraycopy(pluginElements, 0, newElements, hostElements.size, pluginElements.size)
// 將生成的新值賦給 "dexElements" 屬性
dexElementsField.set(hostPathList, newElements)
}
這樣就合并了兩個(gè)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()
// 調(diào)用上面的load方法
Toast.makeText(this, "請(qǐng)先點(diǎn)擊加載apk", Toast.LENGTH_LONG).show()
}
}
時(shí)間關(guān)系,今天就講到這里,還有兩個(gè)問題(加載資源圖片和四大組件),留到下一篇文章再講
源碼已經(jīng)上傳到github,有需要的可以看一下:
好了,下面來(lái)看一下運(yùn)行效果吧!
