点赞关注,不再迷路,你的支持对我意义重大!
Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
- 随着应用功能模块的增多,组件化和插件化的需求日益强烈;
- 在这篇文章里,我将分析 实现插件化的基本原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
前置知识
反射: 「Java 路线」反射机制(含 Kotlin)
动态代理: 「Java 路线」| 动态代理 & 静态代理
类加载: Java 虚拟机| 类加载机制
so 库加载: 「NDK 路线」| so 库加载到卸载的全过程
资源加载: 【点赞催更】
1. 类加载的委派模型
Java 类加载是一种委托机制(parent delegate),即:除了顶级启动类加载器(bootstrap classloader)之外,每个类加载器都有一个关联的上级类加载器(parent 字段)。当一个类加载器准备执行类加载时,它首先会委托给上级加载器去加载,而上级加载器可能还会继续向上委托,递归这个过程。如果上级构造器无法加载,才会返回由自己加载。
更多内容:类加载: Java 虚拟机| 类加载机制
2. Android 中的类加载器
在 Java 中,JVM 加载的是 .class 文件,而在 Android 中,Dalvik 和 ART 加载的是 dex 文件。这里的 dex 文件不仅仅指 .dex 后缀的文件,而是指携带 classed.dex 项的任何文件(例如:jar / zip / apk)
这一节我们就来分析 Android ART 虚拟机 中的类加载器:
ClassLoader 实现类 | 作用 |
---|---|
BootClassLoader | 加载 SDK 中的类 |
PathClassLoader | 加载应用程序的类 |
DexClassLoader | 加载指定的类 |
2.1 BootClassLoader
在 Java / Android 中,BootClassLoader 是委托模型中的顶级加载器,作为委托链的最后一个成员,它总是最先尝试加载类的。它是 ClassLoader 的非静态内部类,源码如下:
ClassLoader.java
class BootClassLoader extends ClassLoader {
public static synchronized BootClassLoader getInstance() {
单例
}
public BootClassLoader() {
没有上级类加载器
super(null);
}
@Override
protected Class> findClass(String name) {
注意 ClassLoader 参数:传递 null
return Class.classForName(name, false, null);
}
@Override
protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException {
1、检查是否加载过
Class> clazz = findLoadedClass(className);
2、尝试加载
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
Class.java
static native Class> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) throws ClassNotFoundException;
要点如下:
- 1、BootClassLoader 是单例的,一个进程只会有一个 BootClassLoader 对象,并在 JVM 启动的时候启动;
- 2、BootClassLoader 的 parent 字段为空,没有上级类加载器(可以通过判断一个 ClassLoader#getParent() 是否来空来判断是否为 BootClassLoader);
- 3、BootClassLoader#findClass(),最终调用 native 方法,我在 第 节 再说。
2.2 BaseDexClassLoader
在 Android 中,Java 代码的编译产物是 dex 格式字节码,所以 Android 系统提供了 BaseDexClassLoader 类加载器,用于从 dex 文件中加载类。
BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
从 DexPathList 的路径中加载类
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
throw new ClassNotFoundException(...);
}
return c;
}
添加 dex 路径
public void addDexPath(String dexPath, boolean isTrusted) {
pathList.addDexPath(dexPath, isTrusted);
}
添加 so 动态库路径
public void addNativePath(Collection libPaths) {
pathList.addNativePath(libPaths);
}
}
可以看到,BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这个 DexPathList 指定了搜索类和 so 动态库的路径。
2.3 PathClassLoader & DexClassLoader
从源码可以看出,PathClassLoader & DexClassLoader 其实都没有重写方法,所以主要的逻辑还是在 BaseDexClassLoader。
这两个类其实只有一点不同,在 Android 9.0 之前,DexClassLoader 的构造方法需要传入第二个参数optimizedDirectory
,这个路径是存放优化后的 dex 文件的路径(odex)。
不过在 Android 9.0 之后,DexClassLoader 也不需要传这个参数了。
参数 | 描述 |
---|---|
dexPath | 加载 dex 文件的路径 |
optimizedDirectory | 加载 odex 文件的路径 |
librarySearchPath | 加载 so 库文件的路径 |
parent | 上级类加载器 |
DexClassLoader.java - Android 8.0
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
DexClassLoader.java - Android 9.0
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
3. DexPathList 源码分析
在 第 2 节里,我们提到 BaseDexClassLoader 将 findClass() 的任务委派给 DexPathList 对象处理,这一节我们就来分析 DexPathList 里的处理过程。
DexFile 是 dex 文件在内存中的映射
Elment - dexFile
Element[] dexElements 一个app所有的class文件都在 dexElements里
4. 插件化的基本流程
4.1 如何加载插件中的类?
4.1.1 生成 dex 文件
- 1、将
dx.bat
文件添加到环境变量
sdk
├─ build-tools
├── 28.0.2
├── dx.bat
dx.bat
是用于生成 dex 文件的命令,将它添加到环境变量里使用起来会方便些。
2、javac 命令编译 略
3、dx 命令生成 dex 文件
dx --dex --output=「输出文件名.dex」 「com.xurui.test.class」
- 4、将 dex 文件放置在 sdcard(外部存储) 略
4.1.2 使用 DexClassLoader 加载 dex 文件
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
context.getCacheDir().getAbsolutePath(),
null,
context.getClassLoader());
4.1.3 执行类加载
try {
执行类加载
Class> clazz = dexClassLoader.loadClass("com.xurui.test");
...
} catch (Exception e) {
e.printStackTrace();
}
4.2 加载插件的步骤
- 1、创建插件的 DexClassLoader 类加载器;
- 2、获取宿主 App 的 PathClassLoader 类加载器;
- 3、合并两个类加载器中的 dexElements,生成新的 Element[];
- 4、通过反射将新值赋值给宿主的 dexElements 字段。
1、宿主类加载器
ClassLoader appClassLoader = context.getClassLoader();
宿主 DexPathList
Object appPathList = pathListField.get(appClassLoader);
宿主 dexElements
Object[] appDexElements = (Object[]) dexElementsField.get(appPathList);
2、插件加载器
ClassLoader pluginClassLoader = new DexClassLoader(apkPath,
context.getCacheDir().getAbsolutePath(),
null,
appClassLoader);
插件 DexPathList
Object pluginPathList = pathListField.get(pluginClassLoader);
插件 dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
3、合并 dexElements
// Object[] obj = new Object[appDexElements.length + pluginDexElements.length]; // x
Object[] newElements = (Object[]) Array.newInstance(appDexElements.getClass().getComponentType(),
appDexElements.length + pluginDexElements.length);
System.arraycopy(appDexElements, 0, newElements, 0, appDexElements.length);
System.arraycopy(pluginDexElements, 0, newElements, 0, pluginDexElements.length);
4、赋值
dexElementsField.set(appPathList, newElements);
5. 启动插件中的四大组件
5.1 矛盾
在 第 4 节 中,我们已经成功实现了插件中类的加载。但是对于四大组件来说,由于插件中的组件没有在宿主AndroidManifest.xml
中注册,即时完成了类加载,也无法启动。
5.2 解决策略
解决策略是使用一个代理 Activity 作为中转,实现偷天换日:
- 1、在宿主 App 中注册「ProxyActivity」;
- 2、Hook AMS 中启动 Activity 的流程,将 「启动 PluginActivity」修改为「启动 ProxyActivity」;
- 3、Hook AMS
使用动态代理和反射机制可以实现 Hook,而在寻找 Hook 点时需要遵循以下原则:
- 1、尽量 Hook 静态变量或单例变量(不容易被改变);
- 2、尽量 Hook public 的对象和方法(影响范围最小)。
5.3 实现步骤
提示: 以下源码基于 Android Q - API 26。
1、注册 ProxyActivity 略
2、Hook AMS
1、获取 singleton 对象
Class> amsClazz = Class.forName("android.app.ActivityManager");
Field singletonField = amsClazz.getDeclaredField("IActivityManagerSingleton");
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
2、获取 IActivityManager 对象
Class> singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstance = mInstanceField.get(singleton);
3、动态代理 IActivityManager
Class> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxyInstance = Proxy.newProxyInstance(context.getClassLoader(),
new Class[]{iActivityManagerClazz},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
5、修改 Intent,替换 PluginActivity 到 ProxyActivity
6、不修改原有执行流程
return method.invoke(mInstance, args);
}
});
4、反射修改字段
mInstanceField.set(singleton, proxyInstance);
public static final String EXTRA_TARGET_INTENT = "target_intent";
5、修改 Intent,替换 PluginActivity 到 ProxyActivity
5.1 过滤
if ("startActivity".equals(method.getName())) {
int indexOfIntent = -1;
for (int index = 0; index < args.length; index++) {
if (args[index] instanceof Intent) {
indexOfIntent = index;
break;
}
}
5.2 启动 PluginActivity 的Intent
Intent pluginIntent = (Intent) args[indexOfIntent];
5.3 启动 ProxyActivity 的Intent
Intent proxyIntent = new Intent();
proxyIntent.setClassName("com.xurui", "com.xurui.ProxyActivity");
args[indexOfIntent] = proxyIntent;
5.4 保存原本的 intent
proxyIntent.putExtra(EXTRA_TARGET_INTENT, pluginIntent);
}
6、不修改原有执行流程
return method.invoke(mInstance, args);
- 3、Hook Handler
1、创建 Handler.callback
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
// msg.obj == ActivityClientRecord
switch (msg.what) {
case 100: // LAUNCH_ACTIVITY
try {
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
1.1 获取 proxyIntent
Intent proxyIntent = (Intent) intentField.get(msg.obj);
1.2 替换为 pluginIntent
Intent pluginIntent = proxyIntent.getParcelableExtra(EXTRA_TARGET_INTENT);
if (null != pluginIntent) {
intentField.set(msg.obj, pluginIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 不改变原有流程
return false;
}
};
2、获取 ActivityThread 对象
Class> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);
3、获取 mH 对象
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);
4、赋值
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, callback);
- 4、版本适配
针对每个版本的源码,需要分别对 Hook 点进行适配。
6. 加载插件中的资源
资源加载:asset / res
通过resource访问,其实也是通过assetmanager去访问
2020年12月27 暂停
7. 总结
创作不易,你的「三连」是丑丑最大的动力,我们下次见!