在后台隐形屏上替你操作 App:解构 Mika 的 VirtualDisplay 焦点隔离、本地 SPAKE2 配对与 PRoot 沙箱架构
随着大语言模型(LLM)与多模态模型(VLM)在推理与工具调用能力的飞跃,移动端操作系统上的“自主代理”(Mobile Agent)成为了极客圈最热门的方向之一。然而,长期以来市面上绝大多数 Android 端侧自动化方案——无论是早期的 AutoJS、Appium 脚本流派,还是近年来在学术界颇为火热的各类基于多模态截屏与无障碍服务的 Agent 实验项目——在真实日常使用中几乎都会沦为无法落地的“工程玩具”。其根本痛点在于:它们无一例外都要霸占手机的物理主屏幕。只要 Agent 开始执行任务,你的手机屏幕就会疯狂闪烁、按键自动翻滚、软键盘频繁弹出;你既不能回复微信,也不能浏览网页,稍有误触就会导致 Agent 的执行流彻底崩溃。
开源项目 Mika(由开发者 wangzan101 主导并完全开源)以一种极其惊艳的 Android 底层系统工程架构,彻底终结了这一痛点:它借助 AOSP 的虚拟显示(VirtualDisplay)技术在后台独立开辟出一块“看不见的虚拟屏”,结合 Android 14 引入的独立焦点隔离标志,让 Agent 在后台隐蔽静默地操作 App,而用户在主屏上的打字、滑屏与多任务切换完全不受丝毫干扰。更令人震撼的是,它彻底摒弃了繁琐的电脑连接与第三方特权工具,在手机内部以纯 Kotlin 实现了 mDNS 探测、TLS 1.3 握手与 SPAKE2 配对协议,仅凭本地无线调试即可完成免 Root 特权提权,甚至还在 Android 端侧深度内嵌了打满内核级补丁的 PRoot Linux 沙箱。在这篇文章中,我将深入其底层源码,全景拆解 Mika 如何在移动操作系统受限沙盒内构建起这套令人惊叹的工业级端侧 Agent 体系。
1. 痛点根源:移动端 Agent 为何长期沦为“桌面玩具”?
在过去很长一段时间里,我一直在测试各类移动端 AI 自动化方案。只要进入真实的多任务日常场景,传统的自动化架构就会暴露出四重难以克服的系统级硬伤:
- 主屏焦点残酷争夺(Focus Hijacking):传统方案直接在默认屏幕(
Display.DEFAULT_DISPLAY)上调用startActivity或模拟触摸。一旦 Agent 启动目标应用或触发输入框,主屏幕的全局焦点立即被强行抢走。如果用户此时正在输入法中打字,软键盘会瞬间收起或输入被打断,体验极其灾难。 - 截屏与渲染的性能陷阱(Screenshot Overhead):传统无障碍服务调用
AccessibilityService.takeScreenshot()在许多 OEM 定制系统(如澎湃 OS、ColorOS、OriginOS 等)上存在严重的内存泄露、主线程阻塞以及偶发性黑屏 Bug;若使用MediaProjection系统投屏通道,则必须长期常驻一条刺眼的高危投屏系统通知,且每次启动都需要用户手动弹窗授权。 - 特权提权的外部依赖鸿沟(Privilege Gap):为了获取模拟全局触摸、跨应用读取剪贴板、免杀后台进程等高级权限,过往方案要么要求设备已解锁 Bootloader 并刷入 Magisk/KernelSU,要么强依赖电脑 USB 调试,或者要求用户额外安装配置 Shizuku。对于普通用户而言,准入门槛高耸入云。
- 端侧计算与本地操作的断层(Sandbox Absence):Agent 在处理提取到的长文本、PDF 表格、复杂正则或文件转换时,受制于 Android 应用受限的 Dalvik/ART 运行环境,无法原生执行灵活的 Python 脚本或 Node.js 数据清洗逻辑;如果将数据统统回传到云端解析,不仅耗尽用户流量,更带来了难以承受的隐私泄露风险。
Mika 的出现,正是针对这四个死结逐个击破的降维打击。
2. 全局架构:双屏分立、特权守护与四层执行管道
为了看清 Mika 的系统脉络,我先梳理出其整体的技术分层拓扑。Mika 在架构上严格划分为 “物理主屏(用户交互域)”、“后台虚拟屏(Agent 调度域)”、“本地特权守护进程(Shell 提权层)” 与 “本地 Linux PRoot 沙箱(端侧计算层)”:
flowchart TD
subgraph UserSpace ["用户主交互域 (Physical Display 0)"]
UserUI["Compose 聊天交互界面 / 悬浮胶囊"]
MiniView["实时小画面预览 (Live Mini-Pip ≤10fps)"]
UserWork["用户正常聊微信 / 刷网页 / 打字"]
end
subgraph AgentCore ["Mika 应用主进程 (com.wangzan101.mika, UID 10xxx)"]
AgentLoop["AgentLoop (感知-思考-工具循环)"]
ScreenReader["ScreenReader (A11y 节点归一化 + FNV 哈希)"]
PhoneOcr["PhoneOcr (离线双引擎 ML Kit OCR)"]
LocalAdb["原生 Kotlin ADB 客户端 (TLS 1.3 + SPAKE2)"]
ImageReader["ImageReader 帧捕获 (RGBA_8888 硬件直写)"]
end
subgraph PrivilegedShell ["特权助手进程 (app_process, UID 2000 Shell)"]
HelperMain["HelperMain 常驻守护"]
ShellService["ShellService (IMikaShell.Stub)"]
VDisplay["VirtualDisplay 控制器"]
InputInjector["InputManager 输入注入 (Tap/Stroke/Key)"]
AudioMuter["AudioMuteLedger (appops 静音控制)"]
end
subgraph LinuxSandbox ["端侧运行环境 (PRoot 5.4.0 + Alpine Linux)"]
PRoot["libproot.so (打满 4 项内核调用补丁)"]
Rootfs["Alpine 3.22 最小根文件系统"]
Runtimes["Python 3.12 / Node.js / Shell 离线环境"]
end
UserUI -->|下达任务目标| AgentLoop
AgentLoop -->|跨进程 AIDL 调用| ShellService
ShellService -->|创建并挂载 Surface| VDisplay
VDisplay -->|离屏渲染像素直通| ImageReader
ImageReader -->|缩放推流| MiniView
ImageReader -->|高保真捕获| ScreenReader & PhoneOcr
ScreenReader & PhoneOcr -->|结构化状态反馈| AgentLoop
LocalAdb -->|配对与拉起| HelperMain
ShellService -->|模拟输入| InputInjector
ShellService -->|音频管控| AudioMuter
AgentLoop -->|派发离线代码处理| LinuxSandbox
PRoot --> Runtimes
style UserSpace fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#fff
style AgentCore fill:#0f2e24,stroke:#10b981,stroke-width:2px,color:#fff
style PrivilegedShell fill:#3b1e2b,stroke:#f43f5e,stroke-width:2px,color:#fff
style LinuxSandbox fill:#2d1f42,stroke:#a855f7,stroke-width:2px,color:#fff
3. 双屏分立与焦点隔离:基于 VirtualDisplay 的隐形战舰
Mika 能够实现“在后台干活而不打扰用户前台”的核心技术底牌,就在于它对 Android 底层显示系统(DisplayManager)与窗口管理器(WindowManager)的深度榨取。
3.1 苛刻的 Display 标志位组合
在 app/src/main/java/com/wangzan101/mika/cap/phone/shell/ShellService.kt 中,ShellService 接到指令后,会通过反射获取系统隐藏的 DisplayManager 实例,并在 Shell(UID 2000)上下文下创建虚拟屏:
internal fun displayFlags(sdk: Int, trusted: Boolean): Int {
var flags = FLAG_PUBLIC or FLAG_OWN_CONTENT_ONLY or FLAG_SUPPORTS_TOUCH or
FLAG_ROTATES_WITH_CONTENT or FLAG_DESTROY_CONTENT_ON_REMOVAL
if (sdk >= Build.VERSION_CODES.TIRAMISU) {
flags = flags or FLAG_TOUCH_FEEDBACK_DISABLED
if (trusted) flags = flags or FLAG_TRUSTED or FLAG_OWN_DISPLAY_GROUP or FLAG_ALWAYS_UNLOCKED
}
// 关键核心:Android 14+ 焦点解耦
if (sdk >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && trusted) {
flags = flags or FLAG_OWN_FOCUS or FLAG_STEAL_TOP_FOCUS_DISABLED
}
return flags
}
这段逻辑精妙至极,蕴含着对 Android 焦点机制的深刻洞察:
FLAG_OWN_FOCUS:为该虚拟屏幕赋予独立的焦点树(Focus Stack),意味着运行在这块屏上的 App 能够拥有合法的聚焦窗口、光标与选中状态。FLAG_STEAL_TOP_FOCUS_DISABLED(核心硬核机制):在 Android 14(API 34)之前,即使一块屏幕具有FLAG_OWN_FOCUS,一旦其内部拉起新的 Activity,系统也会默认将其提升为全系统的顶级焦点屏(Top Focused Display),导致物理主屏的焦点瞬间失落。而设置了该标志后,虚拟屏上的任何应用启动、页面跳转或弹窗,被物理硬性禁止剥夺主屏幕的顶级焦点!用户在物理屏上打字的手指完全感受不到后方的剧烈变动。- 输入法本地隔离(
DISPLAY_IME_POLICY_LOCAL = 0):在虚拟屏聚焦到文本输入框时,系统默认会将软键盘窗口仅渲染在虚拟屏本地,主屏幕绝不会莫名其妙弹出一个半屏键盘。
3.2 零拷贝的 ImageReader 画面捕获
在图像流水线上,Mika 并没有采用开销巨大的系统截图广播,而是将应用进程内的一个原生 ImageReader 对应的 Surface 直接作为入参传给了特权服务:
// AgentDisplay.kt
private val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
// 通过 AIDL 跨进程递交给 ShellService
shell.createDisplay(reader.surface, width, height, dpi)
虚拟屏合成的所有 GPU 帧,通过底层 Binder 句柄直接投递进当前进程的 ImageReader 中。Mika 内部设计了严格的限速节流机制(≤ 10 fps),在 acquireLatestImage() 消费过程中丢弃中间旧帧,既能实时以极低的 CPU 占用生成对话卡片上的画中画小画面(缩放至宽 ≤ 360 像素),又能随时提供 RGBA_8888 的高保真原始位图供 OCR 和多模态模型解析。
3.3 细节防扰:静音守护与剪贴板伪装
为了让后台操作达到真正意义上的“无感隐形”,Mika 在细节上的防御性工程令人叹为观止:
- 静音隔离(
AudioMuteLedger):后台应用刷视频或看广告时很容易发出杂音。Mika 在启动后台应用前,先通过特权通道查询其PLAY_AUDIO状态,并将其强制重置为ignore(appops set <pkg> PLAY_AUDIO ignore),并在本地留存状态账本;任务结束后精确还原原始音频模式。 - 剪贴板静默覆盖(
ShellClipboard):许多应用只允许系统输入法或剪贴板粘贴文本。Mika 在借用剪贴板写入待填入的文字时,会附带android.content.extra.IS_SENSITIVE与com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY标志,从而彻底屏蔽了 Android 13+ 系统底部的“已复制到剪贴板”气泡浮层,并在粘贴完成后自动将用户原有的剪贴板内容无缝还原。 - 主屏溢出拦截(
watchMainScreen):后台应用偶尔会触发跳出操作(例如点击微信支付跳转到外部应用,或呼出系统拨号盘),系统可能会将新任务默认生成在主屏上。Mika 在每个动作前后都会通过watchMainScreen监视主屏的任务栈顶;一旦发现非预期的应用冒泡到主屏,立即将其拉回虚拟屏,或者在检测到密码/支付等关键节点时优雅停下,呼叫用户接管。
4. 零电脑特权飞跃:纯 Kotlin 手搓 mDNS 与 SPAKE2 本地配对
在以往的 Android 自动化生态中,想要拉起一个持有 UID 2000 的常驻 Shell 进程,通常需要依赖电脑 adb shell 命令。而 Mika 在这一领域展现了极为罕见的纯端侧工程实现力——它直接把一套完整的 ADB 协议栈用纯 Kotlin 搬进了 Android APK 内部。
整个本地自举授权与进程握手流程如下图所示:
sequenceDiagram
autonumber
participant App as Mika 主进程 (UID 10xxx)
participant mDNS as 本机 Android mDNS 服务
participant adbd as 本地 adbd 守护 (端口随机)
participant Helper as 后台特权进程 (UID 2000)
Note over App: 用户开启开发者选项中的“无线调试”
App->>mDNS: 监听 `_adb-tls-pairing._tcp` 广播
mDNS-->>App: 回报配对监听端口 P1
App->>adbd: 建立 TLS 1.3 连接 (P1)
Note over App,adbd: 双方以 6 位配对码导出密钥,执行 SPAKE2 密钥协商
App->>adbd: 发送 RSA 公钥并交换 AES-GCM PeerInfo
adbd-->>App: 配对完成,写入授权设备白名单
App->>mDNS: 监听 `_adb-tls-connect._tcp` 广播
mDNS-->>App: 回报连接服务端口 P2
App->>adbd: 建立 TLS 1.3 连接 (P2) 并完成 ADB CNXN 握手
App->>adbd: 发送 Shell 命令:exec app_process ... HelperMain
Note over adbd,Helper: Fork 启动特权进程 (UID 2000, NiceName: mika:helper)
App->>Helper: 通过标准输入 (stdin) 写入一次性安全随机 Nonce,立即关闭管道
Helper->>Helper: 校验 Mika APK 签名的 SHA-256 指纹
Helper->>App: 经由 ContentProvider (content call) 交出 IMikaShell Binder 句柄
Note over App,Helper: 跨进程 IPC 通道建立完毕,进入常驻协同
4.1 SPAKE2 密码交换实现细节
在 core/src/main/kotlin/com/wangzan101/mika/core/adb/AdbPairing.kt 中,Mika 实现了与 Android adbd 规范完全对齐的配对流程:
- 客户端通过 mDNS 服务发现解析出
_adb-tls-pairing._tcp的局域网端口; - 建立 TLS 1.3 握手,并使用 RFC 5705 规范从 TLS 会话中导出 64 字节密钥素材(Label 为
"adb-label\0"); - 将用户在系统弹窗中看到的 6 位配对数字码与导出的密钥素材拼接,作为 SPAKE2 对称密码协商的输入密钥;
- 双方通过 Diffie-Hellman 点运算完成认证,并用派生出的 AES-GCM 密钥加密互换双方的
PeerInfo(包含客户端生成的 ADB RSA 公钥)。
整个过程完全在手机环回网络(127.0.0.1)中毫秒级闭环,用户只需在配对浮窗中填入一次 6 位数字,手机即可完成自举配对!
4.2 严密的跨进程交接安全链
Shell 进程拥有读取屏幕、跨进程注入操作的高级特权,如何防止恶意应用假冒 Mika 窃取特权句柄?Mika 设计了三重密不透风的安全防线:
- Stdin 瞬时阅后即焚:启动
HelperMain时,握手口令(Launch Nonce)绝不作为命令行参数暴露在ps进程列表中,而是通过标准输入流(stdin)写入一行,读取完毕后立即System.in.close()销毁。 - 签名指纹强绑定(Certificate Pinning):Helper 进程在拿到 Binder 准备回传前,会显式调用 PackageManager 提取宿主包的签名证书,并与编译时硬编码的 SHA-256 指纹进行比对。如果宿主包名被恶意假冒或重新签名,Helper 立即自杀并销毁所有改动。
- 心愿存留与保活恢复(
RestoreWish):Android 系统的 Low Memory Killer(LMK)机制可能会随时杀掉前台的 Mika 界面。Helper 进程作为独立的特权进程不仅能在 Mika 崩溃后主动擦除屏幕、解除静音,更持有 Mika 的RestoreWish标记。一旦发现 Mika 异常身亡,Helper 能够将其无障碍服务重新写回系统设置,并触发 Provider 将 Mika 重新拉起,展现出极强的工业级韧性。
5. 多层次感知与状态机:如何让 Agent 看懂每一步?
移动端界面远比网页 DOM 结构更动态且碎片化。原生控件、自定义自绘(Canvas)、跨平台框架(Flutter / React Native)以及 Web 嵌套层出不穷。Mika 在状态感知层构建了三道互补的防线:
5.1 归一化 A11y 树与 FNV 变更哈希
在 ScreenReader.kt 中,Mika 将系统无障碍窗口树自顶向下递归展开,剥离出输入法(TYPE_INPUT_METHOD)与系统浮层,并将所有物理像素坐标换算为 [0..1000] 的归一化浮点区间,生成易于大模型理解的元素引用标号(如 @e1、@e2)。
更关键的是其变动检测算法:
hash = mix(hash, n.className, n.text, n.contentDescription, rect.flattenToString(), states(n))
它通过快速 FNV 哈希算法计算全屏可见节点的文本、类型、绝对位置、焦点与勾选状态。在模型执行点击操作后,Mika 无需死等固定的 Thread.sleep,而是能够实时比对哈希值:一旦哈希改变,立刻确认界面发生转移并进入下一步思考;若哈希持续不变,则触发重试或诊断模式。
5.2 离线双模型 ML Kit OCR 兜底
对于游戏界面、小程序自绘 Canvas 或完全没有设置无障碍标签(contentDescription 为空)的应用,ScreenReader 往往只能看到一个空荡荡的 View 根节点。
此时,PhoneOcr.kt 自动激活:它在设备端并行调用两套 Google ML Kit 文字识别引擎(中文识别 ChineseTextRecognizerOptions 与拉丁文识别 TextRecognizerOptions),完全离线在本地运行。识别出的文本框同样被归一化为 [0..1000] 坐标,无缝拼接到上下文中。当遇到复杂的非标准图标或纯图片按钮时,再按需切入多模态模型(VLM)进行像素级视觉定位。
6. 端侧全功能 Linux 沙箱:PRoot 的内核级补丁战役
在很多复杂任务中(例如:“把微信群里最近的 PDF 财报下载下来,提取出净利润并生成汇总表格发给我”),单纯的 App 点击是无法办到的,必须依赖端侧的代码计算能力。Mika 为此在 Android(仅限 arm64 设备)内部塞入了一个完整的 Alpine Linux 3.22 最小根文件系统,并集成了 Python 3.12 与 Node.js 运行时。
在没有 Root 权限的 Android 用户空间运行 Linux 根文件系统,唯一的技术解法是使用 PRoot(利用 ptrace 拦截并转译系统调用)。然而,原生 PRoot 5.4.0 诞生年代较早,在现代 Android 11~15 严苛的 seccomp 过滤器面前几乎“见光死”。
Mika 的工程团队在此处打下了 4 个极为硬核的 C 语言内核级补丁,堪称 Android 系统逆向与虚拟化领域的教科书级实践:
| 补丁文件 | 攻克痛点与解决机制 |
|---|---|
proot-0001-exitkill.patch |
防止沙箱子进程僵死孤儿化:为 PRoot 的跟踪选项注入 PTRACE_O_EXITKILL。确保当 PRoot 启动器因超时或被系统杀死时,Linux 内核能连带杀光所有受其跟踪的沙箱进程,彻底避免子进程在后台偷跑算力或电池。 |
proot-0002-seccomp-trap-enosys.patch |
化解 Android 现代 seccomp 过滤器引发的崩溃:Android 系统为普通应用装配了极其严苛的 seccomp 规则,对新系统调用(如 io_uring、clone3、faccessat2、rseq 等)直接抛出 SIGSYS 信号杀进程。该补丁在 si_code == SYS_SECCOMP 触发处直接将返回值拦截伪造成 -ENOSYS 并抹除终止信号,迫使 musl libc 退回到兼容的老旧系统调用,化险为夷。 |
proot-0003-arm64-new-path-syscalls.patch |
扩展 arm64 路径转译调用表:原生 PRoot 5.4.0 的 arm64 调用号仅维护到 statx(291),导致后续的新调用(如 faccessat2、openat2、execveat)直接穿透到内核真实路径,导致文件寻址混乱。补丁不仅补全了调用号,更强制屏蔽了可能绕过路径转译的危险调用。 |
proot-0004-link2symlink-fstat-nonfile.patch |
修复 Node.js 运行时随机 Abort 严重缺陷:PRoot 的 link2symlink 扩展在遇到管道、套接字(pipe:[...])等非文件描述符时,因读取未初始化的内存栈垃圾数据,会把原本成功的 fstat 误报为 EPERM,导致 Node.js 启动直接崩溃。补丁修正了判断逻辑,彻底稳定了 Node.js 运行基石。 |
配合随包分发的最小镜像(rootfs.tar.xz),Mika 可以在完全物理断网的环境下,让大模型编写 Python 脚本提取处理用户的文件,计算完毕后直接输出到手机的“下载/Mika”目录。既赋予了 Agent 强大的数据计算能力,又牢固守住了个人数据绝不出手机的隐私底线。
7. 电脑协同与 MCP 架构:把手机变成 AI 桌面集群的执行终端
Mika 不仅是一个独立的手机 App,更是一座连接桌面 AI 生态的桥梁。在很多场景下,我们习惯在电脑端使用 Claude Code、Cursor、Antigravity 等强大的 AI 编码助手,而这些桌面 Agent 往往被困在电脑的浏览器与本地终端里,无法触碰现实世界中的移动端生态。
Mika 在代码库中提供了开箱即用的桌面扩展方案(tools/mika),支持两种轻量接入姿态:
- CLI + Agent Skill 极速模式:无需在电脑端全局安装庞大的环境,利用随仓库分发的
mika_cli.py脚本,电脑端 Agent 即可通过 USB 或本地 Wi-Fi 发送控制目标(Goal)、流式读取状态(Status)、应答手机端的人机交互提问(Answer),或者在任务进行中动态修正要求(Steer)。 - 模型上下文协议(Model Context Protocol, MCP):通过 ADB 端口转发建立带有令牌校验的安全通道,向 Claude Desktop 等标准 MCP 客户端直接暴露手机操作工具(
mika_task_run、mika_device_list等)。
这使得诸如“读取电脑代码仓库,在手机钉钉上向相关人员发送告警,同时在美团上为通宵加班的攻城狮点一份外卖”这样横跨电脑与移动设备的多 Agent 复合协同任务,首次具备了工程落地的可行性。
8. 总结:重新审视端侧 Agent 的工程艺术
在深入拆解完 Mika 的全部代码后,我感受到了巨大的技术震撼。
很多时候,人们容易产生一种错觉:认为大模型时代的 Agent 开发,无非就是写几段 Prompt、封装几个 API、调一调 Function Calling。但在极其封闭、碎片化、严苛沙盒化的移动操作系统面前,缺乏底层系统工程底蕴的“纸上谈兵”,注定会在真实硬件的摩擦中撞得粉碎。
Mika 的卓越之处,在于它没有逃避任何一个脏活、累活与硬骨头:
- 它深挖 AOSP 源码,用冷门晦涩的
FLAG_STEAL_TOP_FOCUS_DISABLED巧妙开创了 “双屏分立、无感打扰” 的全新交互范式; - 它不屑于依赖外部第三方工具,用纯 Kotlin 手搓了 TLS 1.3 和 SPAKE2,打造了 零电脑、零 Root 的单机无线自举特权通路;
- 它敢于直面 Android Linux 内核与 seccomp 的铁律,手写 4 个汇编与 C 语言内核补丁,在掌中设备上硬生生搭建起 安全隔离的 PRoot 本地计算沙箱。
这种敢于直面底层机制、在系统缝隙中以极致工程创造奇迹的极客精神,正是开源社区最迷人、最震撼人心的力量所在。
原文链接与参考资料
- Mika 官方 GitHub 开源仓库:wangzan101/Mika: An on-device AI agent for Android that operates apps and the web to get things done for you.
- Mika 官方中文架构与功能指南:Mika Documentation: 能做什么详细说明
- Mika 手机无线调试配对与故障排查规范:Mika Setup Guide: 手机操作设置指南
- Android AOSP VirtualDisplay 官方规范:Android Developers: DisplayManager.createVirtualDisplay API Reference
- IETF RFC 5705 规范:Keying Material Exporters for Transport Layer Security (TLS)
- PRoot 官方开源项目与实现:PRoot - Ptrace-based chroot and mount --bind implementation
- Alpine Linux 最小根文件系统发布规范:Alpine Linux Releases: Mini Root Filesystem