Android 动态加载技术的研究

更新 2026-02-03 00:28 UTC+8

前言

动态加载技术广泛用于插件加载、热更新、模块化
搭配 Hook 技术可实现运行前动态注入

本章不作底层深入研究

所有测试在 Android 8.1(SDK 27, MIUI 11) 和 Android 12(SDK 31, MIUI 14) 通过

动态加载 Dex

加载 Dex 到新的 ClassLoader

Java
1
2
3
4
5
6
String dexFilePath = "path/to/file.dex";

File odexDir = getDir("dex", Context.MODE_PRIVATE);
DexClassLoader dexClassLoader = new DexClassLoader(dexFilePath, odexDir.getAbsolutePath(), null, getClassLoader());

Class<?> clazz = dexClassLoader.loadClass("tc.InternalClass");

加载 Dex 到全局 ClassLoader

通过反射把 Dex 文件添加到应用程序的 dexPath

Java
1
2
3
4
5
6
7
8
9
10
11
12
String dexFilePath = "path/to/file.dex";

ClassLoader classLoader = getClassLoader();

Field pathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);

Method addDexPath = pathList.getClass().getDeclaredMethod("addDexPath", String.class, File.class);
addDexPath.invoke(pathList, dexFilePath, null);

Class<?> clazz = classLoader.loadClass("tc.InternalClass");

公有目录(/sdcard/)下的 Dex 文件具有可写权限,出于安全,一般安卓设备不被允许加载可写文件,因此需要复制到内部私有目录再设置只读后加载

Java
1
2
3
4
String internalDexFilePath = getCacheDir().getAbsolutePath() + "/a.dex";
fileCopy(dexFilePath, internalDexFilePath);
new File(internalDexFilePath).setReadOnly();
addDexPath.invoke(pathList, internalDexFilePath, null);

动态加载 SO (原生共享库)

直接加载

Java
1
System.load("/path/to/libinternal.so");

添加 SO 文件目录到全局

通过反射把 SO 文件目录添加到应用程序的 nativePath
此方法需要 Android 9.0(SDK 28) 及以上系统

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String soFilesDir = "path/to/libs/";

ClassLoader classLoader = getClassLoader();

Field pathList = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
pathList.setAccessible(true);

// 方法 1
Method addNativePath = pathList.getClass().getDeclaredMethod("addNativePath", Collection.class);
ArrayList<String> dirList = new ArrayList<>();
dirList.add(soFilesDir);
addNativePath.invoke(pathList, dirList);

// 方法 2
Field libDirsField = pathList.getClass().getDeclaredField("nativeLibraryDirectories");
libDirsField.setAccessible(true);
List<File> dirList2 = (List<File>) libDirsField.get(pathList);
dirList2.add(new File(soFilesDir));
libDirsField.set(pathList, dirList2);

System.loadLibrary("internal");

已经加载过的类使用的是旧的 pathList,因此无法使用 System.loadLibrary() 加载 SO
目录里的所有 SO 文件都可被加载

公有目录 SO 文件的处理方法同 Dex 文件

动态加载资源文件(Assets, Resources)

通过反射添加外部 APK/ZIP 的资源文件到全局

此方法会覆盖应用原有资源
此方法会把 APK/ZIP 中的 assets/, res/, resources.arsc 添加到应用,因此需要处理资源 ID 重复的问题

Java
1
2
3
4
5
6
7
8
9
String apkPath = "/sdcard/test.apk";
// 或是一个 ZIP 文件的路径

AssetManager am = getAssets();
Method addAssetPath = am.getClass().getDeclaredMethod(
"addAssetPath", String.class);
addAssetPath.invoke(am, apkPath);

// 现在可直接像一般资源文件一样访问新的资源了

添加外部资源不用处理是否可写的问题,因此可以直接添加公有目录的 APK/ZIP 文件

处理资源 ID 重复问题

在模块级 build.gradleandroid.aaptOptions 中添加 additionalParameters

app/build.gradle
1
2
3
4
5
android {
aaptOptions {
additionalParameters '--package-id', '0x02', '--allow-reserved-package-id'
}
}

资源 ID 前缀可用的范围: 0x02 ~ 0x7F,确保应用程序资源前缀和外部资源前缀不重复

参考文献