Android中DexClassLoader加载类后Method.invoke转对象遇ClassCastException
首先明确:直接强制转换是不可行的,但我们可以通过调整类加载策略或者抽象接口的方式解决这个问题。
为什么会抛出ClassCastException?
在Android的ART虚拟机(以及标准JVM)中,一个类的唯一性是由类加载器 + 全类名共同决定的。哪怕两个类的全类名、字节码完全一致,只要是被不同的类加载器加载,它们就会被视为完全不同的类型。
回到你的场景:
- 主APK中的
com.example.library.MyClass是由系统默认的PathClassLoader加载的 - 子APK中的同一个类是由你手动创建的
DexClassLoader加载的
这就导致两个MyClass在虚拟机眼里是完全不兼容的类型,所以强制转换必然失败——你看报错日志里的类名拼写完全一致,但就是转不了,核心原因就在这里。
解决方案一:让两个组件共享同一个类定义(推荐)
我们可以让子APK不打包MyClass的字节码,而是委托主APK的类加载器来加载这个类,这样两者的MyClass就是同一个类加载器实例化的,自然可以正常转换。
具体步骤:
修改子APK的依赖配置
在子APK的build.gradle中,把对Library的依赖从implementation改成compileOnly(旧版本Gradle用provided):// 如果是本地Library模块依赖 compileOnly project(':Library') // 如果是远程Maven依赖 compileOnly 'com.example:library:1.0.0'这样子APK编译时能找到
MyClass的定义,但打包时不会把它的字节码打进APK里。调整DexClassLoader的父加载器
创建DexClassLoader时,不要用SystemClassLoader,改用主APK自身的类加载器(这样子APK加载类时会优先委托父加载器,也就是主APK的类加载器去加载共享的MyClass):// 注意:优化dex的输出目录建议用应用的缓存目录,不要传空字符串 DexClassLoader dexClassLoader = new DexClassLoader( "data/data/com.example.woosuk.mainapplication/sub.apk", getCacheDir().getAbsolutePath(), null, MainActivity.this.getClassLoader() // 主APK的类加载器作为父加载器 );主APK保持原有依赖
主APK继续用implementation或api依赖Library,确保MyClass被主APK的类加载器加载。
做完这些调整后,你原来的强制转换代码就能正常运行了。
解决方案二:用接口抽象解耦
如果因为某些原因不能修改子APK的打包方式,我们可以通过抽象接口的方式避免直接依赖具体类。
具体步骤:
- 在共享Library中定义接口
新增一个IMyClass接口,包含MyClass的所有对外方法:package com.example.library; public interface IMyClass { int getVersion(); void setVersion(int version); String getName(); void setName(String name); } - 让MyClass实现这个接口
修改Library中的MyClass:package com.example.library; public class MyClass implements IMyClass { // 原有代码不变,自动实现接口方法 private int version; private String name; public MyClass(int version, String name) { this.version = version; this.name = name; } @Override public int getVersion() { return version; } @Override public void setVersion(int version) { this.version = version; } @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } } - 修改子APK的方法返回类型
子APK的MyClassInterface中,把getMyClassInstance的返回类型改成IMyClass:public class MyClassInterface { public IMyClass getMyClassInstance() { return new MyClass(99, "test"); } // 原有getIntValue方法不变 public int getIntValue() { return 99; } } - 主APK中转换为接口类型
主APK里不再强制转换为MyClass,而是转换为IMyClass:Method methodGetMyClass = _class.getMethod("getMyClassInstance"); IMyClass myClass = (IMyClass) methodGetMyClass.invoke(object); // 之后调用myClass的getVersion、getName等方法都正常
这种方式通过接口实现了类型兼容,因为接口是由主APK的类加载器加载的,子APK中的接口也会委托父加载器加载,所以类型匹配。
内容的提问来源于stack exchange,提问作者woosuk

