文章内容
- 一、为什么启动插件 Activity 不能像启动普通类那样直接来
- 二、Activity 启动的完整链路
- 三、问题出在第二部分:AMS 的注册校验
- 四、Hook 是什么:在代码的必经之路上安装一个钩子
- 五、怎么选 Hook 点:两个原则
- 六、插件化的思路:占坑法(ProxyActivity 方案)
- 七、Hook 点一:在 AMS 代理上动手
- 7.1 反射拿系统原对象
- 7.2 动态代理替换
- 7.3 第一个 Hook 的验证
- 八、Hook 点二:在 ActivityThread 的 Handler 上动手
- 8.1 为什么要挂 Handler 上
- 8.2 反射拿 mH 和 mCallback
- 8.3 自定义 Callback:还原 intent
- 8.4 第二个 Hook 的验证
- 九、知识点回忆
- 十、小结
本文整理自享学团队直播课《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 到底是怎么被启起来的。
- 第一块:App 进程内。 你调用
startActivity(intent),一路走到ActivityManager.getService()拿到 AMS 的代理,然后通过 Binder 把请求发出去。 - 第二块:AMS(系统进程内)。 请求通过 Binder 到了
ActivityManagerService,它做启动校验(就是检查有没有注册)、创建任务栈、执行启动流程,最后通过 Binder 把"该启动哪个 Activity"的通知发回给 App。 - 第三块:回到 App 进程。 App 收到通知,走
ApplicationThread→ActivityThread→Handler的一系列消息分发,最终调起onCreate。
其中第一块到第二块、第二块到第三块之间,都是通过 Binder 跨进程通信。这一段不是本讲的重点,你只要知道:Activity 的启动被拆成了跨进程的两半,因为 AMS 管着全局的 Activity 调度,App 自己说了不算。

三、问题出在第二部分: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 了,回过头看那个 LoadUtil:
- 系统原来加载的是宿主的
dexElements; - 我们通过反射把它替换成了
newElements(宿主 + 插件的合并数组); - 于是"系统本来要加载 A,结果加载成了 B"——这就是一次 Hook,改动了代码的正常流程。
所以 Hook 并不是什么独立于反射的新魔法,它的实现底座就是反射和动态代理。明白了这一点,你再去读各种插件化、热修复的源码就会发现,套路全是一样的。
五、怎么选 Hook 点:两个原则
Hook 的前提是找一个"钩子要挂的地方",这个位置叫 Hook 点(Hook Point)。不是随便找个方法就 Hook 的,老师给了两个选择原则:
- 尽量找静态变量或单例对象。
- 尽量找 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。
这个过程分两次挂钩子,正好对应启动链路的两端:
- 第一个 Hook(送进去时):在 App 进程内、准备把请求发往 AMS 之前,把"我要启动 PluginActivity"的 intent 替换成"我要启动 ProxyActivity"的 intent。这样 AMS 校验到的是已注册的 ProxyActivity,顺利放行。
- 第二个 Hook(返回来时):在 AMS 放行、通知回到 App 进程之后、真正执行
onCreate之前,把 intent 再改回 PluginActivity。这样最终执行到的是插件的onCreate。
用《西游记》比喻特别贴切:唐僧(PluginActivity)要去西天取经,路上有妖怪拦路(AMS 校验不认它)。唐僧打不过妖怪,就请孙悟空(ProxyActivity)代打。孙悟空把妖怪打死、通关之后,再把真唐僧换回来继续取经。PluginActivity 是唐僧,ProxyActivity 是孙悟空(也就是"占坑"的那个替身),两个 Hook 就像"去路上换一次、回来路上再换一次"。

七、Hook 点一:在 AMS 代理上动手
第一个 Hook 点,我们要改的是"发往 AMS 之前的 intent"。要想改 intent,就得在链路里找到一个能拿到 intent、又好反射的地方。顺着 startActivity 源码往下看,最终会走到:
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() 的实现是:
// ActivityManager.getService()public static IActivityManager getService() { return IActivityManagerSingleton.get();}IActivityManagerSingleton 是 ActivityManager 里的一个静态 Singleton<IActivityManager> 字段,而 Singleton.get() 返回的是它内部缓存的 mInstance。所以我们只要反射拿到这个 Singleton 的 mInstance 字段,就能拿到系统真正在用的那个 IActivityManager 代理对象。反射路径是:
Class.forName("android.app.ActivityManager")→ 拿到 ActivityManager 的 Class;getDeclaredField("IActivityManagerSingleton")→ 拿到那个单例字段(静态的);field.get(null)→ 因为它是静态的,直接拿到Singleton对象;Class.forName("android.util.Singleton")→ 拿到 Singleton 的 Class;getDeclaredField("mInstance")→ 拿到缓存的IActivityManager字段;mInstanceField.get(singleton)→ 拿到系统真正在用的IActivityManager对象。
这里注意,Singleton 是 android.util 下的,不是 java.util。
7.2 动态代理替换
拿到真正的 IActivityManager 对象后,用 Proxy.newProxyInstance 造一个同接口的代理,然后在 invoke 方法里过滤 startActivity,把 intent 换掉:
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 转交给原对象发出去:
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,你会看到打印的是:
我是代理的activity而不是插件的 onCreate。这恰恰说明第一个 Hook 成功了——系统本来要启动 PluginActivity,结果被我们的 Hook 换成了 ProxyActivity,所以起来的是占坑的 ProxyActivity,它打印"我是代理的activity"。出现这个输出的话,说明"送进去时替换"这一步已经握手成功。
八、Hook 点二:在 ActivityThread 的 Handler 上动手
第一个 Hook 把 AMS 骗过去了,AMS 放行后,它会通过 Binder 把"该启动 ProxyActivity"的通知发回 App 进程。回到 App 这一侧后,链路是:
ApplicationThread.scheduleLaunchActivity→ ActivityThread 的 mH(Handler)发送消息→ performLaunchActivity→ Instrumentation→ Activity.onCreate我们第二个 Hook 的目的,就是在"AMS 放行后、真正执行 onCreate 前",把 intent 从 ProxyActivity 再改回 PluginActivity。那这个 Hook 点挂在哪最合适?挂在 ActivityThread 的 mH(一个 Handler)上。

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 的逻辑是:
// 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 塞进去,得先拿到两个对象:
ActivityThread实例——它内部持有一个mH(Handler)字段;mH的mCallback字段——我们要替换它。
ActivityThread 是应用主线程的一个单例,源码里有一个静态字段 sCurrentActivityThread 存着当前的实例。反射路径:
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)这一条消息:
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,你会看到打印的是:
我是插件的mainactivity这才是真正的成功——最终的 onCreate 是插件 MainActivity 的。
这里要提一个坑:插件 Activity 里暂时别用资源(比如 setContentView、R.id.xxx)。因为我们这一讲还没讲资源加载,插件的资源现在根本没被宿主加载进来。如果你在插件 onCreate 里写了 setContentView 或引用 R,一定会崩溃。所以示例里先注释掉资源相关代码,等下一讲资源加载再打开。
// 插件 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:
- 找出 6.0 与 9.0 里这两个 Hook 点的差异(字段名、
what值、Singleton位置等),把本讲 8.0 的代码适配到 6.0 或 9.0。 - 顺着
realStartActivity的源码,画出 6.0 或 9.0 从 AMS 到ApplicationThread的时序图。
本文由 AgentWorkFlow 生成