底层
iphone-use 怎么工作
每台手机在 Mac 上对应一个守护进程。它把屏幕读成无障碍树(文字,不是像素),通过装在 iPhone 上的小型 XCTest runner 发送点击和输入,并在画面稳定后才返回结果。agent 和人走的是同一个守护进程,同一时间只有一方在用手机。
Claude Code / Codex / 任何 MCP 客户端 ─MCP──┐
脚本和其他 agent ─────────────────────HTTP──┤
▼
Mac 上的 iphone-use 守护进程 ──USB 或 Wi-Fi──▶ iPhone 上的 XCTest runner
▲
你,用浏览器或 iOS App ─────────────────────┘
守护进程
守护进程以当前用户的 LaunchAgent 运行,监听 127.0.0.1:44321 并要求密码。它用三种形式提供同样的控制:给 agent 的 MCP server、给脚本的 /agent/* HTTP 接口,以及给人用的网页和 iOS 原生 App(见接入方式)。
- 屏幕读成文字。
/agent/elements返回展平的无障碍树和一个快照令牌;按元素点击绑定在这个快照上,画面变了就返回stale_element_snapshot,不会点错。 - 结果说实话。每个操作都说明是已执行、没发出去还是结果不确定,以及能不能安全重试。点到被其他控件遮住的元素会直接拒绝(
element_occluded)。 - 等画面稳定。动作之后守护进程可以再读一次树,返回变化和一个
settle块(stable、budget_exhausted或observation_failed)。 - 不动 Mac。不碰 Mac 的屏幕、光标和窗口焦点。
XCTest 设备 runner
iphone-use 用自研的 XCTest UI 测试包(runner/)操作手机。它从 v0.14.0 起取代了 WebDriverAgent,接口与 WebDriverAgent 的 HTTP API 兼容,不使用 iPhone 镜像。它是为速度设计的:
- 读屏只做一次无障碍快照,只取固定的几个属性,不再逐个元素、逐个属性地查询。
- 直接合成触摸事件,动作前不等 App 空闲;画面是否稳定由守护进程在动作之后检查。
- 一个小 App 自带 HTTP 服务和 MJPEG 视频流,手机上不需要别的东西。
iphone-use setup 用你的 Xcode 开发团队构建并签名 runner,在手机上启动它,再固定两条本机中继:8100 用于控制,9100 用于画面。中继走 macOS 自带的 usbmuxd(iphone-use relay),所以守护进程只跟 localhost 通信,也不需要 Homebrew 包。
USB 与 Wi-Fi
- USB:插着线时总是优先走 USB。
- Wi-Fi(iOS 17 及以上):拔掉线后,配置、重启和同样的中继通过 CoreDevice 的加密 Wi-Fi 隧道连到手机,不需要额外参数(默认
IPU_RUNNER_TRANSPORT=auto)。这条隧道只由这台 Mac 路由。此时状态显示transport: "wifi-tunnel"。 - iOS 15 和 16 不用升级,但 Xcode 26 起已经看不到这类手机,所以配置走一条自己的旧版路径:挂载对应 iOS 版本的固定 Developer Disk Image,安装 runner,在手机能通过 Wi-Fi 应答时经 lockdown 的 TLS 会话启动它。拔线后通过局域网中继操作,这条路径有签名但不加密:看得到这个 Wi-Fi 流量的人能看到画面,但无法操作手机。这条路径需要付费开发者账号的 App Store Connect API 密钥。
- 不加密的普通局域网中继(
IPU_RUNNER_ALLOW_LAN=1)只是明确选择的最后手段。
请求签名
手机上 runner 的端口会拒绝(401,不做任何事)没有用本次启动令牌签名的请求:签名是对方法、目标、正文、时间戳和一次性随机数做的 HMAC-SHA256。令牌只留在 Mac 上(权限 0600),从不发送,所以截获的请求既不能重放也不能篡改。没有令牌启动的 runner 拒绝一切请求。签名不等于加密,见隐私与安全。
生命周期:空闲释放与预热
- 空闲释放。5 分钟没有 agent 活动、也没人在看画面时,守护进程停掉 runner 并挂起(
IPHONE_USE_IDLE_RELEASE_SECS可调)。下一个 agent 请求会从缓存的构建产物把它拉起来;需要时解锁手机。 - 有锁屏密码的手机保留 runner。这类手机每次启动 runner 都要在手机上输密码,所以空闲释放会让正在运行的 runner 继续运行,并报告
keep_runner_alive。 - 预热。拉起挂起的 runner 在亮屏时约 5 秒,iOS 熄屏后 10–25 秒。为了藏住这段等待,MCP server 启动或会话拿到使用权时,守护进程会在后台先开始拉起,但只针对一小时内被 agent 操作过的手机,且从不在锁屏时进行。
/agent/status是唯一可信来源:drivable、device_state,以及手机被卡住时的hint和setup_blocked_on(锁屏、信任、开发者模式、WARP、Xcode 版本低于手机的 iOS……)。
把手机交还给人
- 同时按住两个音量键即可结束 iOS 的 UI 自动化;runner 会挂起,直到 agent 或网页明确请求。
iphone-use pause会持久地交还给人;iphone-use resume(或网页面板的 agent 模式)结束交还。- 网页工具栏的交还按钮(或
POST /agent/mode {"mode":"human"})会停掉 runner;之后 agent 的输入会得到409 phone_handed_to_human,不会在你手里重新启动 runner。 - 同一时间只有一个使用者。会话用使用权租约(
X-Phone-Owner,默认 300 秒,每次请求续期)占用手机;其他会话得到409 phone_owned和占用者的名字。这是协作会话之间的协调,不是安全边界。
实时画面:浏览器与 iOS App
/phone 页面显示实时画面:由手机上的 runner 编码的 H.264(浏览器不支持 WebCodecs 时用 MJPEG)。单击是点击,拖动是滑动,按住是长按,滚轮滚动,打字用你自己的输入法直接进手机。画面不变时不会重新编码。
| 模式 | 画面 | 静止画面 | 滚动时 |
|---|---|---|---|
| 性能 | 一半尺寸,最高 30 fps | 约 0.04 Mbit/s | 约 1.5 Mbit/s |
| 画质 | 原生尺寸(17 Pro Max 上 1320×2868),约 50 fps | 约 0.1 Mbit/s | 约 6 Mbit/s |
自动模式在本机或同一局域网用画质模式,其他地方用性能模式。在禁止截屏的页面(银行、支付),你看到的是无障碍树的实时线框图,而不是一片空白,照样可以点着操作。
SwiftUI 原生 iOS App(最低部署版本 iOS 15)在 Mac 网页上扫码配对,用系统硬件解码播放 H.264 画面。它不在 App Store 上:iphone-use setup-app 会用你自己的签名团队构建它。
手机上的守护进程与本机控制
手机上的守护进程和 Mac 上是同一个程序,链接进了 runner。有付费开发者团队时,iOS App 里的 packet-tunnel 扩展会在手机本身上启动 runner,手机自己就能提供点击、输入、读屏和 agent 接口,运行时不需要 Mac(iOS 17.4 及以上;第一次配对仍需插一次 USB 线)。免费 Apple ID 得到的是远程 App,用来操作由 Mac 托管的手机。交给守护进程的 agent 任务在其他 App 处于前台时也会继续运行。
不止一台手机
一个守护进程操作一台手机。命名实例(./install.sh --instance lab --udid <UDID>)有自己的一份 App、状态目录、launchd 标签、令牌和端口,不动默认安装。每台手机有独立的守护进程和 runner,两台同时操作时,每台都和单独操作一样快。MCP 调用可以用 instance 参数逐次选择已安装的实例。
模拟器(预览)
runner 也能跑在 iOS 模拟器上,不需要手机、Apple ID 或签名:iphone-use simulator start "iPhone 16 Pro" 会编译并启动所需的一切,然后用 IPHONE_USE_INSTANCE=sim 把 MCP 客户端接上去。模拟器没有相机、App Store 应用、真实推送和 Face ID,需要这些的任务只能在真机上做。
速度
以下是项目自己的实测,来自 README(2026 年 10 月,同一台 iPhone 13,USB 连接):
| iphone-use runner | agent-device | |
|---|---|---|
| 读屏(无障碍树) | 0.08–0.135 秒 | 约 0.7 秒 |
| 点击并等画面稳定 | 1.0 秒 | 2.8–3.3 秒 |
走完整的 agent 接口,在 iPhone 17 Pro Max 上按标签点击并带回稳定后的变化约 1.9 秒,基于 WebDriverAgent 的版本是 4.2 秒。实时画面 27–28 fps。这些是维护者的实测,不是第三方评测。