Android插件化原理,从以下三个问题切入:
- 什么是插件化
- 如何实现插件类的加载
- 如何实现插件资源的加载
插件化技术最初是源于免安装运行APK的想法,这个免安装的APK就可以理解为插件,而支持插件的app,则称之为宿主;一方面减小了安装包的大小,另一方面可以实现 app 功能的动态扩展
- APP的功能越来越多,体积越来越大
- 模块之间的耦合度高,协同开发的沟通成本越来越大
- APP功能变多之后,导致方法数可能会超过65535,APP占用内存过大
- 应用之间的相互调用
- 组件化开发就是将一个APP拆分成多个模块,每一个模块都是一个组件,在开发的过程中可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并为一个APK,这就是组件化开发。
- 插件化和组件化略有不同,插件化是将整个APP拆分成多个模块,这些模块包括一个宿主和多个插件,每个模块都是一个单独的APK,最终打包的时候,宿主APK和插件APK分开打包。(最终online部署发布时,可以根据客户需求选择只发布宿主APK,或者是发布宿主APK和其中需要用到的插件APK)
特性 |
DynamicAPK |
dynamic- load-apk |
Small |
DroidPlugin |
RePlugin |
VirtualAPK |
作者 |
携程 |
任玉刚 |
wequick |
360 |
360 |
滴滴 |
支持四大组件 |
只支持Activity |
只支持Activity |
只支持 Activity |
全支持 |
全支持 |
全支持 |
组件无需在宿主manifest 中预注册 |
× |
√ |
√ |
√ |
√ |
√ |
插件可以依赖宿主 |
√ |
√ |
√ |
× |
√ |
√ |
支持PendingIntent |
× |
× |
× |
√ |
√ |
√ |
Android特性支持 |
大部分 |
大部分 |
大部分 |
几乎全部 |
几乎全部 |
几乎全部 |
兼容性适配 |
一般 |
一般 |
中等 |
高 |
高 |
高 |
插件构建 |
部署aapt |
无 |
Gradle插件 |
无 |
Gradle插件 |
Gradle插件 |
在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进
行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk
如何去实现插件化呢?插件是免安装的,那么要思考几个问题:
- 插件也是APK,APK里面有资源和类,那么需要考虑的问题无非就是两个
- 如何加载插件资源
- 如何加载插件类
- 要加载Android类,as we all konw,四大组件是需要在AndroidManifest.xml中注册的,要在宿主中调用插件的四大组件,显然插件APK中的用到的四大组件相关类,是没有在宿主的AndroidMainfest.xml中注册过的,那么怎么通过宿主去调用插件的四大组件
Java中会通过javac命令将.java文件编译生成.class文件,jvm会加载class文件,解析之后初始化,然后就可以运行了;Android中会将代码编译成一个APK,解压APK可以看到一个或者多个.dex文件,dex文件是DVM的可执行文件,dex就是把class合并优化后生成的(odex);下面我们来学习DVM如何加载dex文件。
Android中是通过ClassLoader加载dex文件的,ClassLoader是一个抽象类,实现主要分为两种类型:系统类加载器和自定义加载器,其实按照我的理解,系统类加载器主要就是BootClassLoader,DexClassLoader和PathClassLoader
- BootClassLoader,用于加载Android Framework层class文件。
- PathClassLoader,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
- DexClassLoader,用于加载指定的dex,以及jar、zip、apk中的classes.dex
类继承关系如下图:
我们先来看下 PathClassLoader 和 DexClassLoader。
注意:有些帖子可能会有如下说法:
DexClassLoader:能够加载未安装的jar、apk、dex等;而PathClassLoader只能够加载系统中已经安装的apk;这个说法是有问题的,或者说针对Android8.0之后的系统,这个说法是错误的
在8.0(API 26)之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了。
看Android源码
// API26之后和API26之前的PathClassLoader的源码是相同的
// /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 {
//从API26开始,super的构造方法,也就是BaseDexClassLoader的构造方法有变动
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
// API 大于26 /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
/**
* @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
*/
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
//可以看到第二个参数optimizedDirectory直接为null,已经和PathClassLoader的构造方法一样了
super(dexPath, null, 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中。
唯一的区别就是第二个参数optimizedDirectory有没有使用,从API26,也就是8.0开始,他们两个的构造函数完全一样,已经没有任何区别,故DexClassLoader能干的事,PathClassLoader也能干
在搞清楚了PathClassLoader、DexClassLoader和BaseDexClassLoader之间的关系之后,我们写一段代码,研究一下PathClassLoader和BootClassLoader之间的关系。
在AS中创建一个Android项目,创建完成之后,再New 一个Module,项目的Project结构视图如下:
在宿主的MainActivity的onCreate方法中加入下代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText("xxxxxxx");
printClassloader();
//invokePluginMethod();
//invokePluginByLoadAPK();
}
private void printlassCloader() {
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.d(TAG, "printlassCloader classloader = " + classLoader);
classLoader = classLoader.getParent();
}
}
输出结果如下:
01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c
从打印结果来看,当前应用程序的类加载器,或者说MainActivity的ClassLoader就是PathClassLoader,而PathClassLoader的parent属性的值是BootClassLoader,需要说明的是,这儿的parent并不是父类,parent是ClassLoader类本身的一个成员属性。
那么在修改一下printClassLoader方法,看看Activity和AppCompactActivity的类加载器具体都是什么:
private void printClassloader() {
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.d(TAG, "printClassloader classloader = " + classLoader);
classLoader = classLoader.getParent();
}
Log.d(TAG,"activity clsloader = "+ Activity.class.getClassLoader());
Log.d(TAG,"appcompactactivity clsloader = "+ AppCompatActivity.class.getClassLoader());
}
打印结果如下:
01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c
01-13 11:55:37.838 13435-13435/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c
01-13 11:55:37.838 13435-13435/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]]
从打印结果可知:
正好印证了,前面谈到的BootClassLoader和PathClassLoader各自的作用范围
说到这儿,可能会有些疑问:
为什么Activity和AppCompactActivity的类加载器不同,需要说明的是BootClassLoader用于加载Android Framework中的类,或者说是BootClassLoader加载的类一定是在Android SDK中的,而AppCompactActivity并不是在SDK中,而是我们在创建项目的时候添加的依赖库,在build.gradle中添加的dependencies,
implementation 'androidx.appcompat:appcompat:1.1.0'
如何利用ClassLoader去加载一个类呢?其实非常简单,在之前创建的项目中有个Module是plugin,在plugin中添加一个java类,叫做PluginTest,如下图:
PluginTest的代码如下:
package com.enjoy.plugin;
import android.util.Log;
public class PluginTest {
private final static String TAG = "Rayman";
private void ShowPluginMsg(String msg){
Log.d(TAG,"PluginTest ShowPluginMsg msg = "+msg);
}
}
现在我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.PluginTest,可以参照如下代码,实现对PluginTest类的加载,在宿主APP的MainActivity中实现如下方法:
private void invokePluginMethod() {
//在这里我们需要先生成PluginTest.dex文件
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/PluginTest.dex",
getCacheDir().getAbsolutePath(), null, getClassLoader());
//注意此处如果替换为PathClassLoader也是可以正常执行的
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/PluginTest.dex",
null,getClassLoader());
try {
//通过DexClassLoader加载PluginTest类
Class> clz = dexClassLoader.loadClass("com.enjoy.plugin.PluginTest");
//用DexClassLoader或者PathClassLoader都是可以的
//Class> clz = pathClassLoader.loadClass("com.enjoy.plugin.PluginTest");
//通过反射调用PluginTest类中的方法
Object obj = clz.newInstance();
Method method = clz.getDeclaredMethod("ShowPluginMsg",String.class);
method.setAccessible(true);
method.invoke(obj,"This is invoke Plugin method...");
} catch (Exception e) {
e.printStackTrace();
}
}
生成dex文件的步骤:
将这个路径配置到环境变量,打开cmd,然后用如下命令生成dex文件:
dx --dex --output=output.dex input.class
使用这个命令需要注意,一定要在包名的外层目录执行,比如要生成前面提到的PluginTest.dex,需要按照下面方式执行,在cmd中先进入PluginTest.class文件的包名外层目录,如下:
进入到这儿之后,执行dx --dex --output=PluginTest.dex com\enjoy\plugin\PluginTest.class
执行完上述命令,就会在MyPlugin\plugin\build\intermediates\javac\debug\classes这个目录下生成PluginTest.dex文件
注意:
执行dx命令,只能在包名的外层目录执行,比如说在cmd里面直接进入到.class的目录MyPlugin\plugin\build\intermediates\javac\debug\classes\com\enjoy\plugin,那么在执行dx命令是会报错,无法生成相应的dex文件。
OK,到这儿dex文件就生成了,也就是生成了ClassLoader可以加载的dex文件,亦即DVM可以执行的dex文件。
接下来只需将PluginTest.dex拷贝或者push到手机的/sdcard中,或者是Android虚拟机的/sdcard目录下(/sdcard/PuginTest.dex),然后执行上面已经写好的invokePluginMethod方法,这里我直接在MainActivity的OnCreate方法中调用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(stringFromJNI());
printClassloader();
invokePluginMethod();
}
运行结果如下:
01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c
01-13 14:43:48.050 13645-13645/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c
01-13 14:43:48.050 13645-13645/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 14:43:48.054 13645-13645/? D/Rayman: PluginTest ShowPluginMsg msg = This is invoke Plugin method...
LOG中的最后一句,就是我们在PluginTest.java中打印出来的,到这儿说明在宿主APP中成功的调用了插件APP中的代码。
需要知道插件代码的包名,类名,方法名,以及参数,并不需要安装插件APP,就可以通过ClassLoader加载dex文件,再利用反射调用插件代码,只需要dex文件就实现了免安装调用app代码的功能。
本章节讲述的是通过dex文件加载调用插件APP的代码,到这儿就暂时结束了,下一章节会讲述ClassLoader类加载机制,双亲委托机制,不用生成dex文件,只需要插件APK就可以通过ClassLoader和反射调用插件APP的代码。
下一章节链接:https://blog.csdn.net/qq_31429205/article/details/103959814