golang 热插拨 插件_从零开始实现一个插件化框架(一)
歡迎關(guān)注專欄:里面定期分享Android和Flutter架構(gòu)技術(shù)知識(shí)點(diǎn)及解析,還會(huì)不斷更新的BATJ面試專題,歡迎大家前來探討交流,如有好的文章也歡迎投稿。
Flutter跨平臺(tái)開發(fā)終極之選?zhuanlan.zhihu.com本文對(duì)應(yīng)視頻教程解析:
https://www.bilibili.com/video/BV1SJ411J7ke?from=search&seid=9319103597340738214?www.bilibili.com什么是插件化
概念
插件化技術(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的功能模塊越來越多,體積越來越大,這樣可以將一些業(yè)務(wù)模塊做成插件化,按需加載,從而減小安裝包的體積
模塊之間的耦合度高,協(xié)同開發(fā)溝通成本越來越大
方法數(shù)目可能超過65535,APP占用的內(nèi)存過大
應(yīng)用之間的互相調(diào)用
組件化與插件化的區(qū)別
組件化開發(fā)就是將一個(gè)app分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件,開發(fā)的過程中我們可以讓這些組件相互依賴或者單獨(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ū)別呢?
插件化實(shí)現(xiàn)
插件apk是沒有安裝的,那么怎么讓宿主去加載它呢?我們知道,一個(gè)apk是有代碼和資源組成的,所以只需要考慮兩個(gè)問題即可:
如何加載插件中的類?
如何加載插件中的資源?
當(dāng)然還有最重要的一個(gè)問題,四大組件如何調(diào)用呢?四大組件是需要注冊(cè)的,而插件apk中的組件顯然不會(huì)在宿主提前注冊(cè),那么如何去調(diào)用它呢?
下面我們就來一步一步解決這些問題
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ū)別的,所以接下來我們主要介紹安卓的 ClassLoader 是如何加載
dex 文件的。
ClassLoader實(shí)現(xiàn)類
在Android中,ClassLoader是一個(gè)抽象類,它的實(shí)現(xiàn)類主要分為兩種類型:系統(tǒng)類加載器(BootClassLoader),和自定義類加載器(PathClassLoader | DexClassLoader)
先看一下ClassLoader加載流程圖:
BootClassLoader
用于加載Android Framework層的class文件,比如 Activity、Fragment,不過需要注意的是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ū)別(有的文章說,PathClassLoader只支持直接操作dex格式文件,而DexClassLoader可以支持.apk、.jar和.dex文件,并且會(huì)在指定的outpath路徑釋放出dex文件。其實(shí)不然,甚至可以說兩者沒有任何區(qū)別)
先放一張ClassLoader類繼承關(guān)系圖,相信都能看懂,就不多講了,下面來看一下PathClassLoader 和 DexClassLoader的源碼:
根據(jù)源碼就可以了解到,PathClassLoader 和 DexClassLoader 都是繼承自 BaseDexClassLoader,且類中只有構(gòu)造方法,它們的類加載邏輯完全寫在 BaseDexClassLoader 中。
其中我們值的注意的是,在8.0之前,它們二者的唯一區(qū)別是第二個(gè)參數(shù) optimizedDirectory,這個(gè)參數(shù)的意思是
生成的 odex(優(yōu)化的dex)存放的路徑,PathClassLoader 直接為null,而 DexClassLoader 是使用用戶傳進(jìn)來的
路徑,而在8.0之后,二者就完全一樣了。
下面我們?cè)賮砹私庀?BootClassLoader 和 PathClassLoader 之間的關(guān)系:// 在 onCreate 中執(zhí)行下面代碼
打印結(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通過打印結(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,就可以通過反射加載一個(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舉例,通過源碼,看一下DexClassLoader類加載器是怎么加載一個(gè)apk中的dex文件的。
通過查找發(fā)現(xiàn),DexClassLoader并沒有加載類的方法,繼續(xù)看它的父類,最后在ClassLoader類中找到了一個(gè)loadClass方法,看來就是通過這個(gè)方法來加載類了:
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;elseloader = this;//native方法return VMClassLoader.findLoadedClass(loader, name); }2、如果沒被找到,就會(huì)從parent中調(diào)用loadClass方法去查找,依次遞歸,如果找到了就返回,如果所有的上級(jí)都沒有找到,又會(huì)調(diào)用到findClass一級(jí)一級(jí)的去查找。這個(gè)過程就是雙親委托機(jī)制
3、 findClass
// -->2 加載器一般都會(huì)重寫這個(gè)方法,定義自己的加載規(guī)則 protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name); }根據(jù)前面的打印結(jié)果我們可以看懂,ClassLoader的最上級(jí)是BootClassLoader,來 看下它是如何重寫的loadClass方法,結(jié)束遞歸的:
class BootClassLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {return Class.classForName(name, false, null);}@Overrideprotected 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是如何加載的,通過查找,其實(shí)現(xiàn)方法在它的父類BaseDexClassLoader中:
// /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java @Override protected Class<?> findClass(String name) throws ClassNotFoundException {// 在 pathList 中查找指定的 ClassClass c = pathList.findClass(name, suppressedExceptions);return c; } public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {super(parent);// 初始化 pathListthis.pathList = new DexPathList(this, dexPath, librarySearchPath, null); }findClass中有調(diào)用了DexPathList中的findClass方法,繼續(xù):
private Element[] dexElements; public Class<?> findClass(String name, List<Throwable> suppressed) {//通過 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ù)組來盛放Element。到這里加載apk中的類大家是不是就有思路了?
創(chuàng)建插件的ClassLoader加載器(PathClassLoader或DexClassLoader),然后通過反射,獲取插件的dexElements數(shù)組的值
獲取宿主的ClassLoader加載器,通過反射獲取宿主的dexElements數(shù)組的值。
合并宿主和插件的dexElements數(shù)組,生成一個(gè)新的數(shù)組
通過反射將新的數(shù)組重新賦值給宿主的dexElements
實(shí)現(xiàn)方法
廢話不多說,直接上代碼:(我這里使用了kotlin,寫起來感覺方便一些)
這樣就合并了兩個(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
好了,下面來看一下運(yùn)行效果吧!
原文作者:Pan Geng
原文鏈接:https://blog.csdn.net/qq_22090073/java/article/details/103946596
總結(jié)
以上是生活随笔為你收集整理的golang 热插拨 插件_从零开始实现一个插件化框架(一)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bspline怎么使用 python_资
- 下一篇: 基于python的modbus协议编程_