一、VA 的多进程架构
VA 一共会运行在三种进程之中:
-
宿主进程 (
io.virtualapp):即 VA 自身的主进程。 -
Client App 进程 (
io.virtualapp:pN):即在 VA 中运行的各种 App 进程。 -
VA Server 进程 (
io.virtualapp:x):虚拟服务子进程。
关键特性:双开应用没有直接与真实系统服务交互,而是与虚拟服务进行交互。虚拟服务实现了真实系统服务的能力。

VA环境下的运作方式
二、核心通信机制:虚拟服务层
在 Client App 进程和原生 framework services 的通讯过程中,添加了新的一层 VA Server。
-
实现方式:通过
V+原service的命名方式新增了VAMS、VPMS等 VA service。 - 功能:它们仿造了原 framework 的部分功能,用于管理 Client App 的各种会话,并自身再与原生 framework services 通讯。
Hook 代理的作用:
原生AM、mirror.AM、VAM的作用是 hook Client App 的各种方法,具体操作包括:
- 替换方法参数(如包名、uid)。
- 引导方法调用到 VA Server 中。
服务获取流程:
VA Server 端所有的 service 集中由ServiceFetcher提供。Client 端通过Provider.call的跨进程方式获取IServiceFetcher的IBinder句柄,再通过其获取其他 Service 来完成调用。
三、设计动机:以 Activity 启动为例
3.1 问题背景
为什么要设计中间的这层调用呢?我们用 Activity 举例说明:
看过 Activity 源码的同学知道,当我们启动一个新的 Activity 时,它会先经过 AMS。AMS 会处理一系列逻辑,包括:
- 维护 record 记录注册,
- 判断目标进程是否需要创建,
- 处理启动模式 flags 等。
最终,AMS 会回调到 ActivityThread,调用 Instrumentation 的 newActivity 方法完成创建并执行其 OnCreate 生命周期。
3.2 核心矛盾
但是,我们通过 VA 运行的 Client App 存在一个根本性问题:
- 包名/uid 和 VA 宿主是不一致的。
- 并且没有注册在宿主清单文件中。
因此,如果直接启动,就会受到系统服务的严格校验而失败。
3.3 “偷梁换柱”解决方案
VA 通过一套精妙的两步走策略解决了这个问题:
-
第一步(欺骗 AMS):VA 通过预先注册
StubActivity(其包名就是宿主),然后 hook Client App 所有关联启动的方法,将方法中的参数(即启动 intent)替换为宿主的StubActivity。这样,对于 AMS 的感知而言,它只知道启动了一个合法的宿主StubActivity,校验得以通过。 -
第二步(还原 Intent):在 AMS 处理完毕后,VA 再拦截
mH这个 Handler 的启动消息,将 intent 替换回原版 intent,从而完成“偷梁换柱”的过程。
架构优势:而多进程的框架设计,可以让子进程的 crash 不影响宿主进程的运行。
四、关键技术点详解
4.1 Hook 服务代理
- 操作:Hook 服务代理,替换为自定义的系统服务代理。
-
细节:
:pN进程的目标应用 Hook 了所有的系统服务代理,应用进程没有直接与系统服务通信,而是与:x进程的虚拟服务进行通信。 - 实现:与 H 实例注入 callback 对象实现 msg 消息拦截,进行数据替换。
-
源码指引:可查看
InvocationStubManager类。
4.2 :x 进程实现虚拟系统服务
-
操作:
:x进程实现虚拟系统服务。 -
细节:VirtualApp 中运行的 apk 访问系统服务,均为 VirtualApp 在
:x进程模拟的系统服务。 -
流程:上述步骤中 Hook 了服务代理后,服务代理的实现会访问
:x进程提供服务 bunder 代理对象。 -
源码指引:可查看
BinderProvider。
4.3 AMS 服务代理与 ActivityInfo 信息获取
- 操作:AMS 服务代理,ActivityInfo 信息获取。
- 细节:我们 Hook 了 ASM 服务代理,所以每次我们在启动 Activity 的时候都会获取目标应该的 ActivityInfo 数据,获取 ActvityInfo 数据这一个关键步骤。
-
源码指引:可查看
MethodProxies。
4.4 Activity 栈管理
- 操作:Activity 栈管理。
- 细节:VA 采用的也是通过占坑的方式,因为我们的双开应用并没有真实的安装在系统上,所以需要采用占坑的方式,系统启动的是 StubActivity,在应用进程的 H 的 Callback 进行替换为具体的 Activity。
- 透明性:这一替换对系统服务是没有感知的,应为系统是通过 Binder token 来识别某一个 Activity 的。
-
优化点:另外我们可以发现 VA 为每个应用配置了一个标准启动模式,亲和度为
com.lody.virtual.vt的 StubActivity,与 DroidPlugin 不同不需要配置多个启动模式的 Activity。这主要依赖于 VAMS 的栈管理和 Intent Flag 的配合。
4.5 目标应用进程启动与 intent 数据替换(第一个 hook 点)
- 操作:目标应用进程启动与 intent 数据替换(第一个 hook 点)。
-
细节:我们在跳转到
:pN目标应用进程需要将目标应用进程提前启动起来。VAMS 记录了启动的进程列表数据,这样就可以知道新启动的 APP 应该是:pN进程。进程启动之后我们就可以根据targetApp.vpid替换对应的 StubActivity。
4.6 HCallbackStub 通过 intent 数据将 StubActivityRecord 数据替换回来(第二个 hook 点)
- 操作:HCallbackStub 通过 intent 数据将 StubActivityRecord 数据替换回来(第二个 hook 点)。
-
细节:
:PN进程的目标应用户对 H 实例添加 callback (HCallbackStub),这样我们就可以对消息进行处理,callback 会拦截 LAUNCH_ACTIVITY 消息。 - 后续流程:上述步骤执行完成之后会调用系统正常的 handleLaunchActivity 方法,之后会调用 performLaunchActivity 方法传递 ActivityClientRecord 与 customIntent 而 customIntent 为 null。所以 Activity 实例化过程 ActivityClientRecord 是关键点。
- 类加载:上述步骤由于我们已经替换为了插件的 ActivityInfo 信息,所以创建的 LoadAPK 对象为插件的 apk 内存实例对象,获取的 classLoader 对象也是指定了插件的代码路径与 so 路径等,所以使用这个 classLoader 就可以创建目标 Activity 了。
- 资源加载:Activity 在 ContextImpl 对象创建是会创建 Resources 对象。创建 Resources 对象同样是使用 r.packageInfo 对象参数进行初始化,当然传递的均为插件 apk 资源路径数据。
- 核心替换:上述整个过程通过替换 ActivityClientRecord 的 activityInfo 的 applicationInfo 对象为插件的 applicationInfo 信息以方便创建插件的 LoadAPK 对象以方便对类和资源进行加载。
- Hook 实现:而 ActivityInfo 的替换是通过 HOOK AMSProxy 实现的。
-
源码指引:可见
MethodProxies类的 StartActivity 方法。