Android 插件化(二):如何启动插件里的 Activity

文章内容

本文整理自享学团队直播课《Android 插件化(二):Activity加载》。

上一讲解决的是"插件的类能被加载进来",这一讲解决的是"插件的 Activity 能真正被启动"。前者靠类加载器,后者靠 Hook——在 Android 启动 Activity 的链路中间安插一个钩子,把"要启动哪个 Activity"这个信息偷偷换一遍,让一个已注册的占坑 Activity 去骗过 AMS 的校验,等 AMS 放行之后再把真正的插件 Activity 换回来。这就是插件化里常说的"占坑法"(也叫 ProxyActivity 方案)。

一、为什么启动插件 Activity 不能像启动普通类那样直接来

上一讲我们讲了怎么把插件的类塞进宿主,让宿主能 loadClass 出插件里的类。但"能加载类"和"能启动 Activity"是两码事。

插件化要启动一个 Activity,和启动普通类到底差在哪?

  • 普通类:只要类加载器能找到这个类,new 一下就能用,没有任何系统校验。
  • Activity:它属于四大组件。**四大组件里,Activity 是唯一一个在启动前会被 AMS 校验注册的——AMS 会查你这个 Activity 有没有在 AndroidManifest.xml 里注册过。**没注册就直接抛异常,启动失败。

所以插件化不能像处理普通类那样"把它们 merge 进 dexElements 就好了"。类的加载问题解决了,可你这个插件 Activity 在宿主 app 自己的 manifest 里注册了吗?显然没有——插件是单独的 APK,它有自己的一套清单,宿主的 AndroidManifest.xml 里根本不会写这个 Activity(你也没法写,因为插件 APK 是后下载的,宿主编译时根本不知道它的类名)。

插件 Activity 没在宿主 manifest 注册 → AMS 校验不过 → 启动直接失败。

这里要补充一句:AMS 是四大组件的管理类,运行在系统进程(system_server)里,专门负责 Activity 的启动、生命周期调度。它校验"有没有注册"这件事,用的是宿主 APK 的清单信息,跟你的类能不能加载毫无关系。类的加载是 ClassLoader 的事,注册校验是 AMS 的事,两者互不相干。

二、Activity 启动的完整链路

既然要想办法绕过 AMS 的校验,就得先搞清楚 Activity 到底是怎么被启起来的。

  1. 第一块:App 进程内。 你调用 startActivity(intent),一路走到 ActivityManager.getService() 拿到 AMS 的代理,然后通过 Binder 把请求发出去。
  2. 第二块:AMS(系统进程内)。 请求通过 Binder 到了 ActivityManagerService,它做启动校验(就是检查有没有注册)、创建任务栈、执行启动流程,最后通过 Binder 把"该启动哪个 Activity"的通知发回给 App。
  3. 第三块:回到 App 进程。 App 收到通知,走 ApplicationThread → ActivityThread → Handler 的一系列消息分发,最终调起 onCreate。

其中第一块到第二块、第二块到第三块之间,都是通过 Binder 跨进程通信。这一段不是本讲的重点,你只要知道:Activity 的启动被拆成了跨进程的两半,因为 AMS 管着全局的 Activity 调度,App 自己说了不算。

Activity 启动三大块:App 进程 → AMS 进程 → App 进程,中间靠 Binder

三、问题出在第二部分:AMS 的注册校验

上面这条链路里,第二块就是插件 Activity 过不去的关卡。AMS 拿到你的启动请求后,会去查这个 Activity 是否已在系统登记在案(也就是有没有注册)。

  • 对普通 App 来说,它的每个 Activity 都写在 AndroidManifest.xml 里,AMS 一定查得到,所以顺理成章放行。
  • 对插件来说,它的 Activity 在宿主的清单里查不到,AMS 就判定"这个 Activity 不合法",直接拒绝或抛 ActivityNotFoundException。

所以插件化的矛盾摆在这:我想启动一个 AMS 不认识(没注册)的 Activity,但 AMS 只放行它认识的 Activity。

四、Hook 是什么:在代码的必经之路上安装一个钩子

Hook 这个词听起来很玄,其实一点也不复杂。老师给的比喻很到位:

Hook 就是钩子,作用是改变代码的正常执行流程。

正常情况是 A 调用 B,一条直线。Hook 之后变成了 A 先经过钩子,钩子里面你可以决定:是继续调用 B,还是改去调用 C,或者在调用 B 前后插一段自己的逻辑。跟网络转发一样——你访问一个网页,结果被劫持跳到了别的页面,改的正是正常访问流程。Hook 就是这么回事,实现靠的是反射 + 动态代理。

Hook 的原理:A 本来直接调用 B,加钩子之后先经过 Hook,Hook 决定继续走 B 还是拐到 C

话说回来,我们在上一讲其实就已经用过 Hook 了,回过头看那个 LoadUtil:

  • 系统原来加载的是宿主的 dexElements;
  • 我们通过反射把它替换成了 newElements(宿主 + 插件的合并数组);
  • 于是"系统本来要加载 A,结果加载成了 B"——这就是一次 Hook,改动了代码的正常流程。

所以 Hook 并不是什么独立于反射的新魔法,它的实现底座就是反射和动态代理。明白了这一点,你再去读各种插件化、热修复的源码就会发现,套路全是一样的。

五、怎么选 Hook 点:两个原则

Hook 的前提是找一个"钩子要挂的地方",这个位置叫 Hook 点(Hook Point)。不是随便找个方法就 Hook 的,老师给了两个选择原则:

  1. 尽量找静态变量或单例对象。
  2. 尽量找 public 的方法或字段。

为什么?

  • 方便反射以及保证稳定。反射要想成功,要么你能拿到对象,要么它是静态的。静态的 / 单例的对象,你不需要 new 一个实例就能通过 Field.get(null) 拿到,反射起来最省事。而这是系统内部对象,get 一个真实实例往往拿不到。
  • public 更稳。public 是系统对外暴露的接口,谷歌一般不会随意改动它的名字和语义;反之你要是去反射一个 private 的字段,说不定下一个系统版本就把它改了,你就得适配,非常麻烦。找 Hook 点"尽量找 public",就是图个少适配。

当然这两条是"尽量",不是绝对。但多往这两个方向靠,能省掉大量的版本适配工作。

六、插件化的思路:占坑法(ProxyActivity 方案)

现在我们把问题摆在一起。要启动插件的 PluginActivity,但它没注册,过不了 AMS。怎么搞?

思路很简单:在宿主里预先注册一个"占坑"的 Activity(叫 ProxyActivity,代理 Activity),启动插件时先让 Hook 把这个插件 Activity 悄悄替换成占坑 Activity,骗过 AMS;等 AMS 放行、回到 App 进程之后,再让另一个 Hook 把占坑 Activity 还原成真正的插件 Activity。

这个过程分两次挂钩子,正好对应启动链路的两端:

  1. 第一个 Hook(送进去时):在 App 进程内、准备把请求发往 AMS 之前,把"我要启动 PluginActivity"的 intent 替换成"我要启动 ProxyActivity"的 intent。这样 AMS 校验到的是已注册的 ProxyActivity,顺利放行。
  2. 第二个 Hook(返回来时):在 AMS 放行、通知回到 App 进程之后、真正执行 onCreate 之前,把 intent 再改回 PluginActivity。这样最终执行到的是插件的 onCreate。

用《西游记》比喻特别贴切:唐僧(PluginActivity)要去西天取经,路上有妖怪拦路(AMS 校验不认它)。唐僧打不过妖怪,就请孙悟空(ProxyActivity)代打。孙悟空把妖怪打死、通关之后,再把真唐僧换回来继续取经。PluginActivity 是唐僧,ProxyActivity 是孙悟空(也就是"占坑"的那个替身),两个 Hook 就像"去路上换一次、回来路上再换一次"。

插件化占坑法思路:启动插件 Activity 时先替换成代理 Activity 骗过 AMS,再换回插件 Activity

七、Hook 点一:在 AMS 代理上动手

第一个 Hook 点,我们要改的是"发往 AMS 之前的 intent"。要想改 intent,就得在链路里找到一个能拿到 intent、又好反射的地方。顺着 startActivity 源码往下看,最终会走到:

java
ActivityManager.getService().startActivity(intent, ...);

这里的 ActivityManager.getService() 拿到的是 IActivityManager 这个 Binder 接口的代理对象,它内部就是 AMS 的远程接口。而这个对象,恰好符合我们选 Hook 点的两个原则:

  • 它是一个静态单例(内部用 Singleton<IActivityManager> 持有,通过 getService() 拿到);
  • IActivityManager.startActivity 是 public 接口方法。

所以我们就从这里下手:把这个 IActivityManager 的代理对象,替换成一个我们自己造的动态代理对象。这样系统每次调 startActivity 的时候,就会先经过我们的代理 invoke 方法,在那里把 intent 换成 ProxyActivity 的 intent。这就是 Hook 点一。

7.1 反射拿系统原对象

在 Android 8.0(API 26)上,ActivityManager.getService() 的实现是:

java
// ActivityManager.getService()public static IActivityManager getService() {    return IActivityManagerSingleton.get();}

IActivityManagerSingleton 是 ActivityManager 里的一个静态 Singleton<IActivityManager> 字​段,而 Singleton.get() 返回的是它内部缓存的 mInstance。所以我们只要反射拿到这个 Singleton 的 mInstance 字段,就能拿到系统真正在用的那个 IActivityManager 代理对象。反射路径是:

  1. Class.forName("android.app.ActivityManager") → 拿到 ActivityManager 的 Class;
  2. getDeclaredField("IActivityManagerSingleton") → 拿到那个单例字段(静态的);
  3. field.get(null) → 因为它是静态的,直接拿到 Singleton 对象;
  4. Class.forName("android.util.Singleton") → 拿到 Singleton 的 Class;
  5. getDeclaredField("mInstance") → 拿到缓存的 IActivityManager 字段;
  6. mInstanceField.get(singleton) → 拿到系统真正在用的 IActivityManager 对象。

这里注意,Singleton 是 android.util 下的,不是 java.util。

7.2 动态代理替换

拿到真正的 IActivityManager 对象后,用 Proxy.newProxyInstance 造一个同接口的代理,然后在 invoke 方法里过滤 startActivity,把 intent 换掉:

java
public class HookUtil {     // 保存插件真正的 intent,便于第二个 Hook 还原    public static final String TARGET_INTENT_KEY = "target_intent";     public static void hookAMS(Context context) throws Exception {        // 1. 反射拿到 ActivityManager 里的 IActivityManagerSingleton 单例        Class<?> activityManagerClass = Class.forName("android.app.ActivityManager");        Field singletonField = activityManagerClass.getDeclaredField("IActivityManagerSingleton");        singletonField.setAccessible(true);        Object singleton = singletonField.get(null);   // 静态字段,直接传 null         // 2. 反射拿到 Singleton 里缓存的 IActivityManager        Class<?> singletonClass = Class.forName("android.util.Singleton");        Field mInstanceField = singletonClass.getDeclaredField("mInstance");        mInstanceField.setAccessible(true);        Object realIActivityManager = mInstanceField.get(singleton);   // 系统真正用的 AMS 代理         // 3. 用动态代理造一个同接口的代理,拦截 startActivity        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();        Class<?> iActivityManagerClass = Class.forName("android.app.IActivityManager");        Object proxy = Proxy.newProxyInstance(                classLoader,                new Class[]{iActivityManagerClass},                new IActivityManagerInvocationHandler(realIActivityManager, context));         // 4. 把系统用的对象替换成我们的代理        mInstanceField.set(singleton, proxy);    }}

动态代理的 InvocationHandler 大致是这样:注意它要持有 Context,因为构造占坑 intent 时需要宿主包名;还要持有真正的 IActivityManager,因为最后要把 intent 转交给原对象发出去:

java
public class IActivityManagerInvocationHandler implements InvocationHandler {     private Object realIActivityManager;    private Context context;     public IActivityManagerInvocationHandler(Object realIActivityManager, Context context) {        this.realIActivityManager = realIActivityManager;        this.context = context;    }     @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        // 只拦截 startActivity,其它方法一律放行        if ("startActivity".equals(method.getName())) {            // 找出参数里的 Intent(startActivity 有多个重载,得遍历找,别写死下标)            int intentIndex = -1;            if (args != null) {                for (int i = 0; i < args.length; i++) {                    if (args[i] instanceof Intent) {                        intentIndex = i;                        break;                    }                }            }             if (intentIndex >= 0) {                Intent pluginIntent = (Intent) args[intentIndex];   // 插件 Activity 的 intent                 // 构造一个指向占坑 ProxyActivity 的新 intent                Intent proxyIntent = new Intent();                proxyIntent.setClassName(context.getPackageName(),                        ProxyActivity.class.getName());                // 把真正的插件 intent 塞进 extras,留给第二个 Hook 还原                proxyIntent.putExtra(TARGET_INTENT_KEY, pluginIntent);                 // 替换参数里的 intent                args[intentIndex] = proxyIntent;            }        }         // 让原方法继续执行(把替换后的 intent 交给 AMS 代理,走 Binder)        return method.invoke(realIActivityManager, args);    }}

这里有几个关键细节:

  • 为什么遍历找 intent,而不是写死第几个参数? 因为 IActivityManager.startActivity 有好几个重载(startActivity、startActivityAsUser 等),不同重载参数位置不一样。你写死"第二个参数就是 intent"这种硬编码,系统一升级或换一个重载就崩了。所以要用 instanceof Intent 去遍历找出真正的 intent 参数。
  • 为什么要把插件 intent 存进 extras? 因为我们不能让真正的插件 intent 丢了。只有把它留着,第二个 Hook 才能把它取回来,把 ProxyActivity 换回 PluginActivity。这跟上一讲把元素"复制进新数组"是同一个道理——信息不能丢。

7.3 第一个 Hook 的验证

跑一下这个 Hook。注意,到这一步我们只做了第一个 Hook(送进去时替换),还没做第二个 Hook。这时候启动插件的 Activity,你会看到打印的是:

plain text
我是代理的activity

而不是插件的 onCreate。这恰恰说明第一个 Hook 成功了——系统本来要启动 PluginActivity,结果被我们的 Hook 换成了 ProxyActivity,所以起来的是占坑的 ProxyActivity,它打印"我是代理的activity"。出现这个输出的话,说明"送进去时替换"这一步已经握手成功。

八、Hook 点二:在 ActivityThread 的 Handler 上动手

第一个 Hook 把 AMS 骗过去了,AMS 放行后,它会通过 Binder 把"该启动 ProxyActivity"的通知发回 App 进程。回到 App 这一侧后,链路是:

plain text
ApplicationThread.scheduleLaunchActivity→ ActivityThread 的 mH(Handler)发送消息→ performLaunchActivity→ Instrumentation→ Activity.onCreate

我们第二个 Hook 的目的,就是在"AMS 放行后、真正执行 onCreate 前",把 intent 从 ProxyActivity 再改回 PluginActivity。那这个 Hook 点挂在哪最合适?挂在 ActivityThread 的 mH(一个 Handler)上。

AMS 放行后回到 App 进程的调用链:ApplicationThread → ActivityThread 的 mH → H.handleMessage → performLaunchActivity → onCreate,Hook 点就在 mH 的 mCallback

8.1 为什么要挂 Handler 上

顺着源码看:scheduleLaunchActivity 最终会 sendMessage(LAUNCH_ACTIVITY, activityClientRecord)。这个 activityClientRecord(ActivityClientRecord)就是 message.obj,它里面有一个 Intent 字段,现在存的是我们第一个 Hook 换进去的 ProxyActivity 的 intent。

如果我们能拿到这个 message,再拿到 ActivityClientRecord,就能反射拿到它的 intent 字段,把它换成真正的插件 intent。那怎么拿到 message?

启动 Activity 这条消息,最终会走一个 Handler 的 dispatchMessage → handleMessage。而 Handler 里面有一个叫 mCallback 的字段,dispatchMessage 的逻辑是:

java
// Handler.dispatchMessagepublic void dispatchMessage(Message msg) {    if (msg.callback != null) {        handleCallback(msg);    } else {        if (mCallback != null) {            if (mCallback.handleMessage(msg)) {                return;   // 如果 callback 返回 true,就不再往下走了            }        }        handleMessage(msg);    }}

关键就在这:**mCallback 在 handleMessage 之前先执行。**如果我们能塞一个自定义的 Handler.Callback 进去,就能在 handleMessage 跑之前,也就是在真正的 onCreate 之前,把 intent 换掉。而且这个 mCallback 平时是 null(大多数 Handler 都不传 callback),所以替换它是安全的,不会破坏系统原有逻辑。

更妙的是,我们替换成 mCallback 之后,返回 false,系统还会照常去调它自己的 handleMessage,逻辑完全不受影响。这就是 Hook 点二。

8.2 反射拿 mH 和 mCallback

要把自定义 callback 塞进去,得先拿到两个对象:

  1. ActivityThread 实例——它内部持有一个 mH(Handler)字段;
  2. mH 的 mCallback 字段——我们要替换它。

ActivityThread 是应用主线程的一个单例,源码里有一个静态字段 sCurrentActivityThread 存着当前的实例。反射路径:

java
public static void hookHandler() throws Exception {    // 1. 拿到 ActivityThread 的 Class 和它的静态单例    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");    Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");    sCurrentActivityThreadField.setAccessible(true);    Object activityThread = sCurrentActivityThreadField.get(null);   // 静态单例     // 2. 拿到 mH(ActivityThread 里的 Handler)    Field mHField = activityThreadClass.getDeclaredField("mH");    mHField.setAccessible(true);    Handler mH = (Handler) mHField.get(activityThread);     // 3. (可选) 拿到当前 mCallback,便于链式调用/兜底    // 4. 反射拿到 Handler 的 mCallback 字段并替换    Field mCallbackField = Handler.class.getDeclaredField("mCallback");    mCallbackField.setAccessible(true);    mCallbackField.set(mH, new ActivityThreadCallback(mH));}

8.3 自定义 Callback:还原 intent

ActivityThreadCallback 实现了 Handler.Callback 接口,在 handleMessage 里只处理 LAUNCH_ACTIVITY(what == 100)这一条消息:

java
public class ActivityThreadCallback implements Handler.Callback {     @SuppressWarnings("unused")    private Handler originalHandler;   // 原 mH,兜底用     public ActivityThreadCallback(Handler originalHandler) {        this.originalHandler = originalHandler;    }     @Override    public boolean handleMessage(Message msg) {        // 只对 LAUNCH_ACTIVITY 消息动手(Android 8.0 里它的 what 是 100)        if (msg.what == 100) {            // msg.obj 就是 ActivityClientRecord,里面有个 intent 字段            Object activityClientRecord = msg.obj;            if (activityClientRecord == null) {                return false;            }            try {                // 反射拿到 ActivityClientRecord 里的 intent 字段                Field intentField = activityClientRecord.getClass().getDeclaredField("intent");                intentField.setAccessible(true);                 Intent proxyIntent = (Intent) intentField.get(activityClientRecord);                if (proxyIntent == null) {                    return false;                }                 // 取出我们第一个 Hook 塞进去的插件 Intent                Intent pluginIntent = proxyIntent.getParcelableExtra(HookUtil.TARGET_INTENT_KEY);                if (pluginIntent != null) {                    // 把占坑 ProxyActivity 的 intent 换回真正的插件 intent                    intentField.set(activityClientRecord, pluginIntent);                }            } catch (Exception e) {                e.printStackTrace();            }        }        // 返回 false:让系统的 handleMessage 照常执行,不去打断它        return false;    }}

说回 what == 100 这个魔数。它不是随便定的——ActivityThread.H 类里定义了一个常量 public static final int LAUNCH_ACTIVITY = 100;。handleLaunchActivity 对应的消息 what 就是 100(Android 8.0 是 100,其它版本可能有变化,这正是下一讲要做版本适配的原因)。我们只要在回调里判断这条消息,把它的 intent 还原,就够了;其它消息(what 不等于 100)直接返回 false,让系统原样处理。

8.4 第二个 Hook 的验证

两个 Hook 都做好后,再跑一次。这时候启动插件 Activity,你会看到打印的是:

plain text
我是插件的mainactivity

这才是真正的成功——最终的 onCreate 是插件 MainActivity 的。

这里要提一个坑:插件 Activity 里暂时别用资源(比如 setContentView、R.id.xxx)。因为我们这一讲还没讲资源加载,插件的资源现在根本没被宿主加载进来。如果你在插件 onCreate 里写了 setContentView 或引用 R,一定会崩溃。所以示例里先注释掉资源相关代码,等下一讲资源加载再打开。

java
// 插件 MainActivity 的 onCreate 暂时不用资源和布局public class MainActivity extends Activity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        // setContentView(R.layout.activity_main);   // 资源加载还没讲,先注释        Log.d("TAG", "我是插件的mainactivity");    }}

九、知识点回忆

  • 反射:这是 Hook 的底座。没有反射,你拿不到系统内部那个私有的 mInstance、mH、mCallback,也就无从替换。
  • 动态代理:通过 Proxy.newProxyInstance 造一个 IActivityManager 的替身,在 invoke 里拦 startActivity。
  • Hook 技术:改变代码的正常执行流程,本质是反射 + 动态代理。
  • Handler / 消息机制:第二个 Hook 挂在 mH 的 mCallback 上,你要理解 dispatchMessage 先于 handleMessage 执行,才能想到这一点。Android 本身是消息驱动的,onCreate 最终都是靠 Handler 消息调起来的。
  • 四大组件启动流程 / AMS 管理:知道 AMS 会校验注册,才能明白为什么要占坑。
  • Binder(顺带):跨进程通信,贯穿第一块到第二块、第二块到第三块,本讲不深究。

这几块知识,任何一个不扎实,想自己写出插件化都很难。插件化本身不难,但它把反射、动态代理、Handler、Binder、AMS 这些底层知识全用上了。零散背 API 解决不了这种问题,得靠体系建设。

这一讲和上一讲、以及热修复的关系:上一讲是"把插件类合并进宿主 dexElements",这一讲是"用 Hook 骗过 AMS 的注册校验",两者解决的是插件化里不同的问题。而热修复,也是往 dexElements 里塞 dex,只不过它把补丁 dex 放最前面(让补丁类优先命中),跟插件化"宿主在前、插件在后"正好相反。反射、动态代理、Handler、AMS 这些都通了,这些框架的套路都能一眼看穿。

十、小结

这一讲的几个要点:

  • 为什么插件 Activity 不能直接启动:四大组件里的 Activity 受 AMS 校验,必须在宿主 AndroidManifest.xml 里注册,而插件 Activity 注册不了,所以直连会失败。
  • 解决办法是占坑法(Hook):在宿主预注册一个 ProxyActivity,用动态代理 + Handler Callback 两个 Hook 把"要启动谁"的 intent 偷偷换两遍——送进去时换成 ProxyActivity 骗过 AMS,返回来时换回 PluginActivity 执行真正的 onCreate。
  • 两个 Hook 点:① IActivityManager(ActivityManager.IActivityManagerSingleton.mInstance)——送进去时拦截 startActivity;② ActivityThread.mH 的 mCallback——返回来时拦截 LAUNCH_ACTIVITY 还原 intent。
  • 选 Hook 点两原则:静态 / 单例 + public,图的是好反射和少适配。

TODO:

  1. 找出 6.0 与 9.0 里这两个 Hook 点的差异(字段名、what 值、Singleton 位置等),把本讲 8.0 的代码适配到 6.0 或 9.0。
  2. 顺着 realStartActivity 的源码,画出 6.0 或 9.0 从 AMS 到 ApplicationThread 的时序图。

本文由 AgentWorkFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Android 插件化(二):如何启动插件里的 Activity · Yinchen 的博客