黑科技:從零開始實(shí)現(xiàn)一個(gè)插件化框架(一)

什么是插件化

概念

插件化技術(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中的類大家是不是就有思路了?

  1. 創(chuàng)建插件的ClassLoader加載器(PathClassLoader或DexClassLoader),然后通過(guò)反射,獲取插件的dexElements數(shù)組的值
  2. 獲取宿主的ClassLoader加載器,通過(guò)反射獲取宿主的dexElements數(shù)組的值。
  3. 合并宿主和插件的dexElements數(shù)組,生成一個(gè)新的數(shù)組
  4. 通過(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,有需要的可以看一下:

DynamicTest

好了,下面來(lái)看一下運(yùn)行效果吧!

在這里插入圖片描述
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容