插件化缘来java
插件化技术最初源于免安装运行APK的想法,这个免安装的APK就能够理解为插件,而支持插件的APP咱们通常叫宿主。android
插件化解决的问题数组
插件化与组件化app
组件化:组件化开发就是将一个APP分红多个模块,每一个模块都是一个组件,开发的过程当中咱们可让这些组件相互依赖或者单独调试部分组件等,可是最终发布的时候是将这些组件合并统一成一个APK,这就是组件化开发。框架
插件化:插件化开发和组件化略有不一样,插件化开发是将整个APP拆分红多个模块,这些模块包括一个宿主和多个插件,每一个模块都是一个APK,最终打包的时候宿主APK和插件APK分开打包。ide
经常使用插件化框架对比函数
特性 | dynamic-load-apk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
做者 | 任玉刚 | 携程 | wequick | 360 | 滴滴 |
支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件须要/不须要在宿主manifest中预注册 | 不须要 | 须要 | 不须要 | 不须要 | 不须要 |
插件能够/不能够依赖宿主 | 能够 | 能够 | 能够 | 不能够 | 能够 |
支持/不支持PendingIntent | 不支持 | 不支持 | 不支持 | 支持 | 支持 |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎所有 | 几乎所有 |
兼容性适配 | 通常 | 通常 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
咱们知道Java的类加载采用双亲委托的加载机制,有效防止类的重复加载和保护核心类的加载。Android的加载机制与Java的类加载机制相似,但也有区别。Android中的各个类加载器之间的关系以下图所示。组件化
在Android中,在APP运行时,全部的应用自有的包含字节码文件(.class)的dex
文件被包装成一个 Element
对象,放在了一个Element[] elements
数组中,每个element
元素对应一个dex
文件,应用寻找特定某个类时,会从前日后依次遍历该数组,直到找到或遍历到尾。测试
本文讲的简单插件化实现就利用该Element数组实现:新建一个Element[] newElements
数组,在新建的newElements
数组中实现宿主的Element[] hostElements
数组和插件Element[] pluginElements
数组的拼接,并用新的Element
数组替换宿主原有的数组。ui
当系统经过PathClassLoader
去加载应用程序的dex文件中Java类时,PathClassLoader
并无重写loadClass()
方法,因此接下来由PathClassLoader
的父类BaseDexClassLoader
尝试执行加载任务,然而BaseDexClassLoader
也没有重写loadClass()
方法,则依次向上调用父类加载器的loadClass()
方法,父类加载器(由最顶级父类加载器开始尝试loadClass
)在各自的加载范围内尝试加载须要的类,失败以后依次向下调用子类加载器的findClass()
方法(也就是双亲委托机制那一套)。最终会调用到BaseDexClassLoader
的findClass()
方法。
// PathClassLoader 去掉注释后的所有代码,只有两个构造函数
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
复制代码
public class BaseDexClassLoader extends ClassLoader {
// ...
private final DexPathList pathList;
// ...
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
// ...
}
复制代码
在该方法内,程序会调用BaseDexClassLoader
类中私有成员属性DexPathList pathList
的findClass()
方法。
final class DexPathList {
// ...
private Element[] dexElements;
// ...
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
// ...
}
复制代码
在这段代码中,用加强for
循环从前日后去遍历dexElements
数组,该数组即为上面图片中的说起的Element[]
数组。
插件中的MainActivity.java未额外处理,以下:
package com.tongbo.plugin;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
复制代码
Test类用于以后插件引入以后的测试,以下:
package com.tongbo.plugin;
import android.util.Log;
public class Test {
public static void print() {
Log.e("TB", "Test class from com.tongbo.plugin");
}
}
复制代码
plugin\build\outputs\apk\debug
中找到plugin-debug.apk
,并上传到模拟器的/sdcard/
路径。在onCreate()
回调中,调用LoadUtil.loadClass(this)
实现插件的加载(详见3.3.2);
在宿主的主页面,添加一个按钮,并在onClick
属性绑定toStartPlugin(View view)
方法,并在方法中反射调用插件的类的方法Test.print()
;
package com.tongbo.pluginbasic;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LoadUtil.loadClass(this);
}
public void toStartPlugin(View view) {
Log.e("TB", "btn is clicked.");
try {
Class<?> clazz = Class.forName("com.tongbo.plugin.Test");
Method method = clazz.getMethod("print");
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
LoadUtil
类中的loadClass(Context context)
方法是核心的插件载入方法,主要分为如下几个步骤:
dalvik.system.BaseDexClassLoader
的pathList
字段以及dalvik.system.DexPathList
的dexElements
字段并设置访问权限;dexElements
字段的值;dexElements
字段的值;Array.newInstance()
方法建立新的Element[]
,在新建立的Element[]
中利用System.arraycopy()
方法完成数组的拼接;dexElements
字段的值为新建的Element[]
,完成插件加载。package com.tongbo.pluginbasic;
import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class LoadUtil {
private final static String pluginApkPath = "/sdcard/plugin-debug.apk";
public static void loadClass(Context context) {
try {
//TODO:to get 'pathList' field and 'dexElements' field by reflection.
//private final DexPathList pathList;
Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
//private Element[] dexElements;
Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//TODO:to get the value of host's dexElements field.
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object hostPathList = pathListField.get(pathClassLoader);
Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);
//TODO:to get the value of plugin's dexElements field.
DexClassLoader dexClassLoader = new DexClassLoader(
pluginApkPath,
context.getCacheDir().getAbsolutePath(),
null,
context.getClassLoader()
);
Object pluginPathList = pathListField.get(dexClassLoader);
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
//TODO:to add host's dexElements and plugin's dexElements together in a newly created Element array.
Object[] dexElements = (Object[]) Array.newInstance(
pluginDexElements.getClass().getComponentType(),
hostDexElements.length + pluginDexElements.length
);
System.arraycopy(
hostDexElements,
0, dexElements,
0,
hostDexElements.length
);
System.arraycopy(
pluginDexElements,
0,
dexElements,
hostDexElements.length,
pluginDexElements.length
);
//TODO:to set the newly created Element array to the host's dexElements field and done.
dexElementsField.set(hostPathList, dexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
将宿主APP运行起来,点击按钮,控制台将打印以下(成功):