手实现一下动态加载dex

插件化系列之动态加载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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值