Android 动态加载 Activity

动态加载Activity,本质上就是插件化技术 要解决的核心问题。简单来说,就是要让我们的App能够启动一个未经安装 (即没有在宿主App的AndroidManifest.xml中注册)的Activity。

这之所以成为一个难题,是因为Android系统在设计时,出于安全和管理的考虑,对Activity的启动有严格的校验。我们常说的"坑",主要有三个:Manifest校验、资源访问和生命周期管理

🚧 拦路虎:为什么动态加载Activity这么难?

  1. Manifest校验 :这是最直接的一道坎。当你想通过startActivity启动一个组件时,系统服务AMS会去检查这个Activity是否在AndroidManifest.xml中声明了。如果没有,它会毫不客气地抛出ActivityNotFoundException,直接终止你的启动流程。
  2. 资源访问 :插件是一个独立的apk文件,它的资源(比如布局、图片)都打包在自己内部。宿主App默认的Resource对象无法直接访问插件里的资源。如果直接使用R.id.xxx,会因为找不到资源而崩溃。
  3. 生命周期管理 :Activity不是一个简单的Java对象,它有onCreateonResume等一系列生命周期方法,这些方法由系统服务AMS通过跨进程通信来调用。如果我们只是简单地用new关键字创建一个插件Activity对象,它就是一个"孤魂野鬼",没有生命周期,也无法正常显示。

🛠️ 主流解决方案:两种经典的"瞒天过海"之术

为了解决这些问题,社区探索出了两种主流方案,你可以把它们理解为两种不同的"瞒天过海"之术。

方案一:代理Activity模式

这是早期插件化框架(如DL)普遍采用的方案,也被称为"插桩式"。它的核心思想是:找一个"替身"去蒙混过关,然后让"替身"把所有的生命周期事件转发给真正的"主角"

  1. 提前注册"替身" :在宿主App的AndroidManifest.xml中,提前注册一个或多个"万能"的代理Activity(比如叫ProxyActivity)。
  2. "替身"上场 :当你想启动插件里的PluginActivity时,实际上启动的是这个已经注册好的ProxyActivity。这样就能顺利通过AMS的Manifest校验。
  3. "主角"登场 :在ProxyActivity内部,它不会展示自己的内容,而是通过我们自定义的ClassLoader(如DexClassLoader)去加载插件apk中的PluginActivity类,并创建出它的实例。
  4. 转发命令 :最关键的一步是,ProxyActivity需要把自己收到的所有生命周期回调(如onCreateonResume)都转发给这个PluginActivity实例。这样,PluginActivity虽然是个"黑户",但也能像正常Activity一样拥有完整的生命周期。

这种方案通过定义接口 (如DLPlugin)来规范生命周期方法的转发,避免了频繁使用反射带来的性能开销。

方案二:Hook Activity启动流程

这是一种更高级、更彻底的"偷梁换柱"之术,以DroidPlugin、VirtualApk等框架为代表。它的核心思想是:在Activity启动的半路上,神不知鬼不觉地"狸猫换太子"

  1. 第一步:偷梁换柱(欺骗AMS) :在App准备把启动请求发送给AMS之前(也就是Instrumentation.execStartActivity方法里),通过动态代理 等Hook技术,把Intent中指向PluginActivityComponent,偷偷替换成宿主中一个已经在Manifest注册过的StubActivity(占位Activity)。这样,AMS在Manifest里一查,发现这个StubActivity是合法注册的,就放行了。
  2. 第二步:借尸还魂(恢复身份) :当AMS校验通过,准备通知App进程创建Activity时,会在App主线程的Handler(ActivityThread.H)里发送一条消息。我们再次通过Hook技术,拦截到这条消息,并在这千钧一发之际,把消息里记录的StubActivity信息,再偷偷替换回我们真正的PluginActivity信息。
  3. 第三步:正常创建 :接下来,App进程的Instrumentation就会根据我们替换后的信息,用正确的ClassLoader去加载并创建PluginActivity的实例,并正常执行它的onCreate方法。

这种方案对开发者最友好,插件里的Activity甚至不需要继承任何特定的基类,写法和普通Activity一模一样。但它的技术门槛极高,需要对Android系统源码和Hook技术有非常深刻的理解,而且需要小心翼翼地处理各个Android版本之间的差异。

总结与对比

特性 代理Activity模式 Hook启动流程模式
核心思想 用一个在Manifest注册的"替身"Activity来代理插件Activity的所有行为。 在Activity启动过程中,动态修改AMS的校验数据和App的创建数据。
技术难度 中等。主要涉及类加载器、反射和接口定义。 极高。需要对AMS、Instrumentation、Handler等系统机制有深入了解,并熟练运用动态代理等Hook技术。
插件开发 插件Activity通常需要继承或实现特定的基类或接口。 插件Activity与普通开发无异,无任何侵入性。
兼容性 相对较好,主要是对接口的管理。 较差,不同Android版本的系统代码差异可能导致Hook点失效。
代表框架 Dynamic-Load-APK (DL) DroidPlugin, VirtualAPK, Shadow

我们直接进入代码实现。下面我会分别给出"代理Activity模式"和"Hook启动流程模式"最核心的实现代码片段,让你能直观地看到它们是如何运作的。

📝 方案一:代理Activity模式的核心实现

这种模式的核心思路是用一个在 Manifest 中注册的代理 Activity 来管理插件 Activity 的生命周期。插件 Activity 本身更像一个拥有所有业务逻辑的普通 Java 对象。

1. 插件Activity的基类 (BaseActivity)

所有插件中的 Activity 都必须继承这个基类。它的关键作用是持有代理 Activity 的引用 (that),并将所有需要上下文的方法都转发给这个代理去执行。

java 复制代码
// 插件框架中的基类
public class BaseActivity extends Activity implements PluginActivityInterface {
    // 注入的代理Activity,作为真正的上下文
    private Activity that; 

    // 代理Activity通过这个方法将自身注入
    public void attach(Activity proxyActivity) { 
        this.that = proxyActivity; 
    }

    // 所有需要上下文的方法,都转发给 "that"
    @Override
    public void setContentView(int layoutResID) {
        // 关键:使用代理Activity的setContentView
        that.setContentView(layoutResID); 
    }

    @Override
    public View findViewById(int id) {
        return that.findViewById(id);
    }

    // 其他方法如 startActivity, getResources 等,同理...
    
    // 生命周期方法由代理直接调用,这里可以为空
    @Override 
    public void onCreate(Bundle savedInstanceState) { }
}

2. 代理Activity (ProxyActivity)

这个类需要在宿主 AndroidManifest.xml 中注册。它负责加载插件 Activity,并将自己的生命周期事件转发给它。

java 复制代码
// 宿主的代理Activity
public class ProxyActivity extends Activity {
    private PluginActivityInterface pluginActivity; // 持有插件Activity的实例

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 1. 获取插件信息,例如插件APK路径和要启动的类名
        String pluginApkPath = getIntent().getStringExtra("pluginPath");
        String pluginActivityClass = getIntent().getStringExtra("className");

        // 2. 使用自定义的类加载器加载插件类
        DexClassLoader loader = new DexClassLoader(pluginApkPath, 
                getCacheDir().getAbsolutePath(), null, getClassLoader());
        try {
            Class<?> clazz = loader.loadClass(pluginActivityClass);
            // 插件Activity必须是BaseActivity的子类
            pluginActivity = (PluginActivityInterface) clazz.newInstance();
            
            // 3. 关键步骤:将代理Activity自身注入给插件
            pluginActivity.attach(this); 
            
            // 4. 调用插件的生命周期方法
            pluginActivity.onCreate(savedInstanceState); 
            
        } catch (Exception e) { e.printStackTrace(); }
    }

    // 将其他生命周期方法也转发给插件
    @Override
    protected void onResume() {
        super.onResume();
        if (pluginActivity != null) pluginActivity.onResume();
    }
    
    // ... onPause, onDestroy 等同理
}

当你想启动插件中的 PluginMainActivity 时,实际启动的是这个 ProxyActivity,并把目标类名通过 Intent 传给它。


🧙 方案二:Hook启动流程模式的核心实现

这种模式更高级,旨在让插件 Activity 的启动过程与普通 Activity 无异 。它通过两个 Hook 点来"欺骗"系统。这里我们以 Hook AMS(ActivityManagerService)的启动过程为例,展示如何在 Android 9 (API 28) 上实现。

1. Hook点1:拦截发送给AMS的请求 (偷梁换柱)

当应用调用 startActivity 时,请求会经过 InstrumentationActivityManager 发给 AMS。我们在这一层进行拦截,将 Intent 中的目标 Component(插件Activity)偷偷替换成一个在 Manifest 中注册过的占坑Activity (如 StubActivity)。

java 复制代码
// 使用动态代理,代理 IActivityManager 接口
public class AmsHookHelper {

    public static void hookAms(Context context) {
        // 反射获取 ActivityManager 中的单例 IActivityManagerSingleton
        // ... (省略反射获取对象代码) ...

        // 获取原始的 IActivityManager 对象
        Object oldObj = FieldUtil.getField(activityManagerSingleton, "mInstance");

        // 创建动态代理
        Object proxy = Proxy.newProxyInstance(
                oldObj.getClass().getClassLoader(),
                new Class[]{Class.forName("android.app.IActivityManager")},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 如果调用的是 startActivity 方法
                        if ("startActivity".equals(method.getName())) {
                            // 遍历参数,找到 Intent
                            for (int i = 0; i < args.length; i++) {
                                if (args[i] instanceof Intent) {
                                    Intent intent = (Intent) args[i];
                                    
                                    // 保存原始的 Intent (包含插件Activity的信息)
                                    Intent originalIntent = new Intent(intent); 
                                    intent.setClass(context, StubActivity.class); // 替换成占坑的StubActivity
                                    
                                    // 将原始Intent隐藏起来,稍后恢复
                                    intent.putExtra("ORIGINAL_INTENT", originalIntent); 
                                    break;
                                }
                            }
                        }
                        // 执行原方法
                        return method.invoke(oldObj, args);
                    }
                });

        // 将代理对象设置回去
        FieldUtil.setField(activityManagerSingleton, "mInstance", proxy);
    }
}

2. Hook点2:拦截AMS的回调 (借尸还魂)

AMS 处理完启动请求后,会通过 Handler 通知应用进程创建 Activity。我们 Hook 这个 Handler,在创建前将 Intent 再替换回来。

java 复制代码
// 在 Application 或合适的时机调用
public class HCallbackHook {

    public static void hookHandler() {
        // 反射获取 ActivityThread 的 sCurrentActivityThread 和它的 mH 字段 (Handler)
        // ... (省略反射获取对象代码) ...

        Handler mH = (Handler) FieldUtil.getField(activityThread, "mH");
        // 拿到原始的 mCallback
        Handler.Callback originalCallback = mH.mCallback; 

        // 设置我们自己的 Callback
        mH.mCallback = new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                // 当消息是 LAUNCH_ACTIVITY (通常值为100) 时
                if (msg.what == 100) { 
                    // 获取 ActivityClientRecord 对象
                    Object r = msg.obj; 
                    // 反射获取其中的 Intent
                    Intent intent = (Intent) FieldUtil.getField(r, "intent");
                    
                    // 从 Intent 中取出我们之前隐藏的原始 Intent
                    Intent originalIntent = intent.getParcelableExtra("ORIGINAL_INTENT");
                    if (originalIntent != null) {
                        // 关键步骤:把占坑的Intent替换回插件Activity的Intent
                        FieldUtil.setField(r, "intent", originalIntent); 
                    }
                }
                // 继续执行原来的逻辑
                if (originalCallback != null) {
                    return originalCallback.handleMessage(msg);
                }
                mH.handleMessage(msg);
                return true;
            }
        };
    }
}
相关推荐
城东米粉儿2 小时前
Android lancet 笔记
android
zh_xuan2 小时前
React Native 原生和RN互相调用以及事件监听
android·javascript·react native
哈哈浩丶4 小时前
LK(little kernel)-3:LK的启动流程-作为Android的bootloarder
android·linux·服务器
Android系统攻城狮12 小时前
Android tinyalsa深度解析之pcm_get_delay调用流程与实战(一百一十九)
android·pcm·tinyalsa·音频进阶·android hal·audio hal
·云扬·13 小时前
MySQL基于位点的主从复制完整部署指南
android·mysql·adb
千里马-horse14 小时前
Building a Simple Engine -- Mobile Development -- Platform considerations
android·ios·rendering·vulkan
吴声子夜歌14 小时前
RxJava——Subscriber
android·echarts·rxjava
米羊12118 小时前
ThinkPHP 漏洞(下)
android
前路不黑暗@18 小时前
Java项目:Java脚手架项目的 B 端用户服务(十四)
android·java·开发语言·spring boot·笔记·学习·spring cloud