golang 热插拨 插件_从零开始实现一个插件化框架(一)
歡迎關注專欄:里面定期分享Android和Flutter架構技術知識點及解析,還會不斷更新的BATJ面試專題,歡迎大家前來探討交流,如有好的文章也歡迎投稿。
Flutter跨平臺開發終極之選?zhuanlan.zhihu.com本文對應視頻教程解析:
https://www.bilibili.com/video/BV1SJ411J7ke?from=search&seid=9319103597340738214?www.bilibili.com什么是插件化
概念
插件化技術最初源于免安裝運行 apk 的想法,這個免安裝的 apk 就可以理解為插件,而支持插件的 app 我們一般叫宿主。宿主可以在運行時加載和運行插件,這樣便可以將 app 中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實現 app 功能的動態擴展。
我們知道計算機主板就是由一系列的插槽組成的,我們需要什么功能,給它插上對應的芯片或顯卡就可以了,從而實現熱拔插。基于這個原理,軟件方面的熱拔插就是插件化
插件化解決的問題
APP的功能模塊越來越多,體積越來越大,這樣可以將一些業務模塊做成插件化,按需加載,從而減小安裝包的體積
模塊之間的耦合度高,協同開發溝通成本越來越大
方法數目可能超過65535,APP占用的內存過大
應用之間的互相調用
組件化與插件化的區別
組件化開發就是將一個app分成多個模塊,每個模塊都是一個組件,開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發布的時候是將這些組件合并統一成一個apk,這就是組件化開發。
插件化開發和組件化略有不同,插件化開發是將整個app拆分成多個模塊,這些模塊包括一個宿主和多個插件,每個模塊都是一個apk,最終打包的時候宿主apk和插件apk分開打包。
各插件化框架對比
市面上比較流行的插件化框架也有很多,他們之間都有哪些區別呢?
插件化實現
插件apk是沒有安裝的,那么怎么讓宿主去加載它呢?我們知道,一個apk是有代碼和資源組成的,所以只需要考慮兩個問題即可:
如何加載插件中的類?
如何加載插件中的資源?
當然還有最重要的一個問題,四大組件如何調用呢?四大組件是需要注冊的,而插件apk中的組件顯然不會在宿主提前注冊,那么如何去調用它呢?
下面我們就來一步一步解決這些問題
ClassLoader類加載器
以前在講熱修復的時候,我簡單地介紹了一下ClassLoader的加載機制。java源碼文件在編譯后會生成一個class文件,而在Android中,將代碼編譯后會生成一個 apk 文件,將 apk 文件解壓后就可以看到其中有一個或多個 classes.dex 文件,它就是安卓把所有 class 文件進行合并,優化后生成的。
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的源碼:
根據源碼就可以了解到,PathClassLoader 和 DexClassLoader 都是繼承自 BaseDexClassLoader,且類中只有構造方法,它們的類加載邏輯完全寫在 BaseDexClassLoader 中。
其中我們值的注意的是,在8.0之前,它們二者的唯一區別是第二個參數 optimizedDirectory,這個參數的意思是
生成的 odex(優化的dex)存放的路徑,PathClassLoader 直接為null,而 DexClassLoader 是使用用戶傳進來的
路徑,而在8.0之后,二者就完全一樣了。
下面我們再來了解下 BootClassLoader 和 PathClassLoader 之間的關系:// 在 onCreate 中執行下面代碼
打印結果:
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;elseloader = 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 {@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,從而結束了遞歸。
接著往下走,如果所有的parent都沒找到,DexClassLoader是如何加載的,通過查找,其實現方法在它的父類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中有調用了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,寫起來感覺方便一些)
這樣就合并了兩個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()} }時間關系,今天就講到這里,還有兩個問題(加載資源圖片和四大組件),留到下一篇文章再講
源碼已經上傳到github,有需要的可以看一下:
DynamicTest
好了,下面來看一下運行效果吧!
原文作者:Pan Geng
原文鏈接:https://blog.csdn.net/qq_22090073/java/article/details/103946596
總結
以上是生活随笔為你收集整理的golang 热插拨 插件_从零开始实现一个插件化框架(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bspline怎么使用 python_资
- 下一篇: img设置宽高不生效_便宜 好用 不掉盘