插件化系列之动态加载class示例
手实现一下动态加载dex。
我根据加载dex文件的方式的不同,区分为以下三种方式,分别是
1、直接使用DexClassLoader或者PathClassLoader加载apk
2、反射向pathList的成员变量Element[] dexElements 添加我们的dex(常用于热修复)
3、通过继承系统的ClassLoader(BaseDexClassLoader、DexClassLoader、PathClassLoader),重写findClass方法,利用DexFile.loadDex实现动态加载class
接下来,我们动手验证一下这三种动态加载dex的方式:
首先我们创建此类后续动态加载用, 然后打一个apk备用,然后即可把此类删除或者改名防止影响后续测试,备用apk中我放到assets下。利用AS 的Device Explorer将此apk放到我们测试应用的data/data/包名/files目录下,即可进行测试。
public class Test {
private static final String TAG = "Test";
public void test1() {
Log.d(TAG, "test1: ");
}
public void test2() {
Log.d(TAG, "test2: ");
}
}
一、直接使用DexClassLoader或者PathClassLoader加载apk
这种方式比较简单直接调用DexClassLoader/PathClassLoaderde的构造函数将dexpath传入即可完成动态加载
安卓14上Google对动态加载dex增加了进一步的限制:
应用以Android14为目标平台并使用动态代码加载(DCL),则所有动态加载的文件都必须标记为只读,否则 targetSdk=34,在安卓14设备 就会抛出异常 Writable dex file ‘/data/user/0/com.ww.classloaderdemo/files/classloaderdemo-debug.apk’ is not allowed.
try {
//安卓14 动态加载dex,路径不能是sdcard,且需要调用此 file.setReadOnly() 代码,在此之前是可以的
val apkPath: String = filesDir.absolutePath + "/" + "classloaderdemo-debug.apk"
val file = File(apkPath)
file.setReadOnly()
/**
* 这里使用PathClassLoader/DexClassLoader都能实现动态加载class,区别就是使用PathClassLoader没有第二个参数 即odex路径
* 在android 8.0 之前会在DexClassLoader 会在odex目录下生成.odex文件,8.0以后第二个参数就废弃了,所以这两个classloader * 就没有区别了
*/
val dexClassLoader = DexClassLoader(
file.absolutePath, // APK文件路径
file.absolutePath, // odex路径
null,
context.classLoader.parent
)
val className = "com.ww.classloaderdemo.Test"
val loadedClass = dexClassLoader.loadClass(className)
val instance = loadedClass.newInstance()
val method = loadedClass.getDeclaredMethod("test1")
method.invoke(instance)
} catch (e: Exception) {
Log.e(TAG, "loadApkFromSdCard: ", e)
}
执行这段代码 成功打印
2024-05-08 16:18:52.493 27410-27410 Test com.ww.classloaderdemo D test1:
AI写代码
php
运行
1
2、反射向pathList的成员变量Element[] dexElements 添加我们的dex,常用于热修复
上一篇我们介绍过系统通过遍历PathList的dexElements数组 来遍历所有的dex加载class,所以我们可以通过反射拿到这个dexElements 旧的数组,添加我们的Element构建新的dexElements数组后替换掉原来的dexElements数组,即可完成动态加载。
val file = File(dexPath)
file.setReadOnly()
val cacheDir = context.cacheDir
val classLoader = context.classLoader
try {
val pathListField = Objects.requireNonNull(classLoader.javaClass.superclass)
.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField[classLoader]
val dexElementsField = pathList.javaClass.getDeclaredField("dexElements")
dexElementsField.isAccessible = true
val oldElement = dexElementsField[pathList] as Array<*>
val files = ArrayList<File>()
files.add(file)
//执行makePathElements 让我们的补丁包变成一个 Element[]
val makePathElements = pathList.javaClass.getDeclaredMethod(
"makePathElements",
MutableList::class.java,
File::class.java,
MutableList::class.java
)
makePathElements.isAccessible = true
val suppressedExceptions = ArrayList<IOException>()
val newElement =
makePathElements.invoke(null, files, cacheDir, suppressedExceptions) as Array<*>
//要替换系统原本的Element数组的新数组
val replaceElement = oldElement.javaClass.componentType?.let {
java.lang.reflect.Array.newInstance(
it,
oldElement.size + newElement.size
)
} as Array<*>
//补丁包先进去
System.arraycopy(newElement, 0, replaceElement, 0, newElement.size)
System.arraycopy(oldElement, 0, replaceElement, newElement.size, oldElement.size)
dexElementsField[pathList] = replaceElement
val className = "com.ww.classloaderdemo.Test"
val clazz = classLoader.loadClass(className)
val instance = clazz.newInstance()
val method = clazz.getMethod("test1")
method.isAccessible = true
method.invoke(instance)
} catch (e: Exception) {
Log.e(TAG, "installPatch: ", e)
}
执行这段代码 成功打印
2024-05-08 16:18:52.493 27410-27410 Test com.ww.classloaderdemo D test1:
AI写代码
java
运行
1
3、通过继承系统的ClassLoader(BaseDexClassLoader、DexClassLoader、PathClassLoader),重写findClass方法,利用DexFile.loadDex实现动态加载class
构建我们自己的MyClassLoader如下:
public class MyClassLoader extends ClassLoader {
private static final String TAG = "MyClassLoader";
private String mDexOutputPath;
private String mDexPath;
private String mLibPath;
protected File[] mFiles;
protected DexFile[] mDexs;
private boolean mInitialized;
public MyClassLoader(String dexPath, String dexOutputDir, String libPath,
ClassLoader parent) {
super(parent);
if (dexPath == null)
throw new RuntimeException("dexPath为null");
mDexPath = dexPath;
mDexOutputPath = dexOutputDir;
mLibPath = libPath;
ensureInit();
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
try {
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException e) {
Log.d(TAG, "loadClass: 没找着自己来加载");
}
if (clazz != null) {
return clazz;
}
try {
clazz = loadClassBySelf(className);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException ignored) {
}
throw new ClassNotFoundException(className + " in loader " + this);
}
public Class<?> loadClassBySelf(String className) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
ensureInit();
Class clazz;
int length = mFiles.length;
for (int i = 0; i < length; i++) {
if (mDexs[i] != null) {
String slashName = name.replace('.', '/');
clazz = mDexs[i].loadClass(slashName, this);
if (clazz != null) {
return clazz;
}
}
}
throw new ClassNotFoundException(name + " in loader " + this);
}
protected synchronized void ensureInit() {
if (mInitialized) {
return;
}
String[] dexPathList;
mInitialized = true;
dexPathList = mDexPath.split(":");
int length = dexPathList.length;
mFiles = new File[length];
mDexs = new DexFile[length];
for (int i = 0; i < length; i++) {
File pathFile = new File(dexPathList[i]);
mFiles[i] = pathFile;
if (pathFile.isFile()) {
try {
mDexs[i] = DexFile.loadDex(dexPathList[i], dexPathList[i], 0);
} catch (IOException e) {
Log.e(TAG, "ensureInit: ", e);
}
}
}
}
}
将dexPath传入我们自己的myClassLoader,并调用loadClass方法
val file = File(dexPath)
file.setReadOnly()
val myClassLoader = MyClassLoader(dexPath,dexPath,null,context.classLoader.parent)
val clazz = myClassLoader.loadClass("com.ww.classloaderdemo.Test")
val instance = clazz.newInstance()
val method = clazz.getMethod("test2")
method.isAccessible = true
method.invoke(instance)
执行这段代码 成功打印
2024-05-08 16:18:52.493 27410-27410 Test com.ww.classloaderdemo D
Demo 地址:https://github.com/weiwei0928/Demos/tree/main/classloaderdemo
原文链接:https://blog.youkuaiyun.com/qq_36017595/article/details/140842638