跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Android 插件化技术:动态创建 Activity 模式详解

Android 插件化开发中,静态代理模式存在 LaunchMode 限制及资源加载问题。动态创建 Activity 模式通过运行时字节码操作生成标准 Activity 类,解决注册与生命周期问题。对比两种模式原理,分析 DexMaker 动态编译实现、ClassLoader 劫持替换逻辑,并探讨权限、进程及 Service 支持等局限性,为插件化架构选型提供参考。

修罗发布于 2025/2/7更新于 2026/6/218 浏览
Android 插件化技术:动态创建 Activity 模式详解

Android 插件化学习之路(六)之动态创建 Activity

静态代理 Activity 模式的限制

在之前的代理 Activity 模式文章中,我们讨论了启动插件 APK 中 Activity 的两个核心难题。由于插件中的 Activity 未在主项目的 Manifest 中注册,无法经历系统 Framework 层级的一系列初始化过程,最终导致获得的 Activity 实例没有生命周期且无法使用资源文件。

虽然使用代理 Activity 能够解决这两个问题,但它存在一些明显的限制:

  1. 实际运行的 Activity 实例都是 ProxyActivity,并非真正想要启动的 Activity;
  2. ProxyActivity 只能指定一种 LaunchMode,因此插件里的 Activity 无法自定义 LaunchMode;
  3. 不支持静态注册的 BroadcastReceiver;
  4. 往往不是所有的 APK 都可作为插件被加载,插件项目需要依赖特定的框架,并且需要遵循一定的'开发规范'。

特别是最后一点,无法直接把一个普通的 APK 作为插件使用。插件的 Activity 不是标准的 Activity 对象才会有这些限制,使其成为标准的 Activity 是解决问题的关键。而要使其成为标准的 Activity,则需要在主项目里注册这些 Activity。

想到代理模式需要注册一个代理的 ProxyActivity,那么能不能在主项目里注册一个通用的 Activity(比如 TargetActivity)给插件里所有的 Activity 用呢?解决对策就是,在需要启动插件的某一个 Activity(比如 PlugActivity)的时候,动态创建一个 TargetActivity。新创建的 TargetActivity 会继承 PlugActivity 的所有共有行为,而这个 TargetActivity 的包名与类名刚好与我们事先注册的 TargetActivity 一致,我们就能以标准的方式启动这个 Activity。

动态创建 Activity 模式

运行时动态创建并编译一个 Activity 类,这种想法并非天方夜谭。动态创建类的工具包括 DexMaker 等,二者均能实现动态字节码操作。最大的区别在于前者是创建 dex 文件,而后者通常涉及 class 文件的生成。这里我们主要探讨使用 DexMaker 在运行时创建一个编译好并能运行的类,这被称为'动态字节码操作(runtime bytecode manipulation)'。

使用 DexMaker 动态创建一个类

我们可以使用 DexMaker 工具创建一个 dex 文件,之后反编译这个 dex 看看创建出来的类是什么样子。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onMakeDex(View view) {
        try {
            DexMaker dexMaker = new DexMaker();
            // Generate a HelloWorld class.
            TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
            dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
            generateHelloMethod(dexMaker, helloWorld);
            // Create the dex file and load it.
            File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
            if (!outputDir.exists()) outputDir.mkdir();
            ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
            Class<?> helloWorldClass = loader.loadClass("HelloWorld");
            // Execute our newly-generated code in-process.
            helloWorldClass.getMethod("hello").invoke(null);
        } catch (Exception e) {
            Log.e("MainActivity", "[onMakeDex]", e);
        }
    }

    /**
     * Generates Dalvik bytecode equivalent to the following method.
     *    public static void hello() {
     *        int a = 0xabcd;
     *        int b = 0xaaaa;
     *        int c = a - b;
     *        String s = Integer.toHexString(c);
     *        System.out.println(s);
     *        return;
     *    }
     */
    private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
        // Lookup some types we'll need along the way.
        TypeId<System> systemType = TypeId.get(System.class);
        TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);

        // Identify the 'hello()' method on declaringType.
        MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");

        // Declare that method on the dexMaker. Use the returned Code instance
        // as a builder that we can append instructions to.
        Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);

        // Declare all the locals we'll need up front. The API requires this.
        Local<Integer> a = code.newLocal(TypeId.INT);
        Local<Integer> b = code.newLocal(TypeId.INT);
        Local<Integer> c = code.newLocal(TypeId.INT);
        Local<String> s = code.newLocal(TypeId.STRING);
        Local<PrintStream> localSystemOut = code.newLocal(printStreamType);

        // int a = 0xabcd;
        code.loadConstant(a, 0xabcd);

        // int b = 0xaaaa;
        code.loadConstant(b, 0xaaaa);

        // int c = a - b;
        code.op(BinaryOp.SUBTRACT, c, a, b);

        // String s = Integer.toHexString(c);
        MethodId<Integer, String> toHexString
                = TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
        code.invokeStatic(toHexString, s, c);

        // System.out.println(s);
        FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
        code.sget(systemOutField, localSystemOut);
        MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
                TypeId.VOID, "println", TypeId.STRING);
        code.invokeVirtual(printlnMethod, null, localSystemOut, s);

        // return;
        code.returnVoid();
    }
}

在 SD 卡的 dexmaker 目录下找到刚创建的文件,把里面的 classes.dex 解压出来,然后再用 dex2jar 工具转化成 jar 文件,最后再用 jd-gui 工具反编译 jar 的源码,即可验证生成的类结构。

至此,已经成功在运行时创建一个编译好的类。

修改需要启动的目标 Activity

接下来的问题是如何把需要启动的、在 Manifest 里面没有注册的 PlugActivity 换成有注册的 TargetActivity。在 Android 中,虚拟机加载类的时候,是通过 ClassLoader 的 loadClass 方法。而 loadClass 方法并不是 final 类型的,这意味着我们可以创建自己的类去继承 ClassLoader,以重载 loadClass 方法并改写类的加载逻辑。

在需要加载 PlugActivity 的时候,偷偷把其换成 TargetActivity。大致思路如下:

public class CJClassLoader extends ClassLoader {

    @Override
    public Class loadClass(String className) {
        if (当前上下文插件不为空) {
            if (className 是 TargetActivity) {
                // 找到当前实际要加载的原始 PlugActivity,动态创建类(TargetActivity extends PlugActivity)的 dex 文件
                // 从 dex 文件中加载 TargetActivity
                return 动态加载的 TargetActivity;
            } else {
                // 使用对应的 PluginClassLoader 加载普通类
                return 使用 PluginClassLoader 加载;
            }
        } else {
            return super.loadClass(className); // 使用原来的类加载方法
        }
    }
}

不过还有一个问题,主项目启动插件 Activity 的时候,我们可以替换 Activity,但是如果在插件 Activity(比如 MainActivity)启动另一个 Activity(SubActivity)的时候怎么办?插件是普通的第三方 APK,我们无法更改里面跳转 Activity 的逻辑。

其实,从主项目启动插件 MainActivity 的时候,其实启动的是我们动态创建的 TargetActivity(extends MainActivity)。而我们知道 Activity 启动另一个 Activity 的时候都是使用其 startActivity 方法,所以我们可以在创建 TargetActivity 时,重写其 startActivity 方法,让它在启动其他 Activity 的时候,也采用动态创建 Activity 的方式,这样就能解决问题。

动态类创建 Activity 缺陷

动态类创建的方式,使得注册一个通用的 Activity 就能给多个 Activity 使用,对这种做法存在的问题也是明显的:

  1. 配置受限:使用同一个注册的 Activity,所以一些需要在 Manifest 注册的属性无法做到每个 Activity 都自定义配置;
  2. 权限管理:插件中的权限无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;
  3. 进程隔离:插件的 Activity 无法开启独立进程,因为这需要在 Manifest 里面注册;
  4. 稳定性风险:动态字节码操作涉及到 Hack 开发,相比代理模式起来不稳定。

其中不稳定的问题出现在对 Service 的支持上。使用动态创建类的方式可以搞定 Activity 和 BroadcastReceiver,但是使用类似的方式处理 Service 却不行。因为 ContextImpl.getApplicationContext 期待得到一个非 ContextWrapper 的 context,如果不是则继续下次循环,目前的 Context 实例都是 wrapper,所以会进入死循环。

此外,动态生成代码可能带来安全审计风险,且不同 Android 版本对 ClassLoader 和反射机制的限制不同,可能导致兼容性问题。在实际生产环境中,建议结合成熟的开源框架(如 VirtualAPK 或 Tinker)进行二次开发,而非完全手写底层 Hook 逻辑。

代理 Activity 模式与动态创建 Activity 模式的区别

简单地说,最大的不同是代理模式使用了一个代理的 Activity,而动态创建 Activity 模式使用了一个通用的 Activity。

代理模式:使用一个代理 Activity 去完成本应该由插件 Activity 完成的工作。这个代理 Activity 是一个标准的 Android Activity 组件,具有生命周期和上下文环境(ContextWrapper 和 ContextImpl),但是它自身只是一个空壳,并没有承担什么业务逻辑;而插件 Activity 其实只是一个普通的 Java 对象,它没有上下文环境,但是却能正常执行业务逻辑的代码。代理 Activity 和不同的插件 Activity 配合起来,就能完成不同的业务逻辑了。所以代理模式其实还是使用常规的 Android 开发技术,只是在处理插件资源的时候强制调用了系统的隐藏 API,因此这种模式还是可以稳定工作和升级的。

动态创建 Activity 模式:被动态创建出来的 Activity 类是有在主项目里面注册的,它是一个标准的 Activity,它有自己的 Context 和生命周期,不需要代理的 Activity。这种方式更接近原生体验,但实现复杂度更高,且受限于 Android 系统的安全机制。

总结

动态创建 Activity 模式为 Android 插件化提供了一种更灵活的路径,它通过运行时字节码操作解决了 Activity 注册和生命周期问题。然而,该方案在权限控制、进程管理及 Service 支持方面存在显著短板。开发者在选择架构方案时,应权衡开发成本、维护难度及系统兼容性。对于追求高稳定性和长期维护的项目,建议优先评估成熟的组件化或插件化框架,谨慎引入动态字节码生成技术。

目录

  1. Android 插件化学习之路(六)之动态创建 Activity
  2. 静态代理 Activity 模式的限制
  3. 动态创建 Activity 模式
  4. 使用 DexMaker 动态创建一个类
  5. 修改需要启动的目标 Activity
  6. 动态类创建 Activity 缺陷
  7. 代理 Activity 模式与动态创建 Activity 模式的区别
  8. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • ChatGPT 如何利用结构化原则实现高效信息管理
  • 软考数据库系统工程师:排序算法核心原理与备考指南
  • OSCP 实战笔记:密码攻击之获取并破解 Net-NTLMv2 哈希(上)
  • 火影忍者主题网页设计实战——从布局到动效实现
  • WordPress 设置固定链接后 Apache 无法访问网页的解决方案
  • AI 模型调优与 Python 实战
  • Python 零基础学习路线:如何达到自主接单与就业水平
  • OpenCV 基础教程:绘图、几何变换与图像运算
  • Linux 管道通信实战:匿名管道进程池与命名管道服务端模型
  • WebGL 无代码 3D 交互设计平台:翠鸟艺术家技术解析
  • 从 Webhook 到 OpenClaw:钉钉周报提醒机器人的技术演进
  • Stable Diffusion 基石:潜在扩散模型(LDMs)技术详解
  • Python 高效清理 Excel 空白行列:原理与实战
  • Milvus 实战:Attu 可视化安装与 Python 整合指南
  • Flutter 三方库 flutter_google_maps_webservices 鸿蒙化适配指南
  • Rust 异步微服务架构最佳实践与反模式规避
  • 多模态大语言模型时代的数学推理研究:基准、方法与挑战
  • AI 自动生成一线与二线产区标准图
  • MCP Server 案例:Excel 表格一键生成可视化图表 HTML 报告
  • 深度可分离卷积原理详解及 OPPORTUNITY 数据集实战

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online