一台电脑拖着几十台 iPhone 干活,这套做法叫苹果群控。它不越狱,也不往手机上装东西,靠的是把操作指令从外部注入设备。
适用版本:中控 EasyClick iOS USB 10.7.0+ | 手机系统 iOS 17+ 技术栈:EasyClick iOS USB 中控 + JavaScript 脚本(也支持 Python 等语言走 HTTP 接口) 面向场景:苹果群控(一对多批量控制 iPhone)的底层链路实现 官方文档:https://ieasyclick.com/iosdocs/ | USB HID 函数手册:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/
iOS 自动化难在哪,USB HID 凭什么免签名
安卓有 AccessibilityService(无障碍服务),能直接读界面元素、模拟点击;iOS 没这个待遇,系统封闭,没给第三方开放的无障碍接口。所以在 iPhone 上做自动化只有两条路线。第一条是软件注入:装一个代理 App,通过 XCTest 在 App 内部注入指令,问题是需要签名——签完还要装描述文件、手动信任开发者,签名有期限、过期就得重来,App 或系统一升级代理容易失效。第二条是模拟外部输入设备:让电脑或开发板冒充真实的鼠标键盘,通过系统底层输入通道把触摸事件送进去,iOS 无法区分真假,所以更干净。这篇讲的 USB HID 就是第二条里最省事的一种:一根 USB 数据线就够,不用额外买开发板;一台中控电脑能同时带多台 iPhone,设备从三台加到三十台,代码不用改。
HID 是 Human Interface Device(人机接口设备),鼠标、键盘、游戏手柄都属于它。HID 设备通过标准描述符向主机声明自己是什么类型、支持哪些输入输出,这个规范公开,且被包括 iOS 在内的所有主流操作系统原生支持。关键在于,当一个设备通过 USB 声明自己是 HID 键盘时,iOS 会无条件信任它——电脑端软件构造出符合 HID 规范的报文,告诉手机“我刚按下了屏幕坐标 (300, 500) 的位置”,手机的输入系统就把它当成一次真实的触摸事件分发下去,对系统和 App 来说这跟手指点上去没有区别。真手指和假手指,系统分得开吗?分不开,这正是这条路能走通的原因。
同样思路还有另外两种实现方式:
| 链路 | 硬件依赖 | 系统要求 | 适合场景 |
|---|---|---|---|
| USB HID | 一根 USB 数据线 | iOS 17+,中控 10.7.0+ | 最省事,单机快速验证 |
| 蓝牙 BLE HID | ESP32C3 开发板 | iOS 18+ 更优 | 配合无截图模式可完全绕开屏幕镜像 |
| OTG HID | ESP32S3 开发板 | 通用 | 想脱离电脑独立运行 |
三种方式的 API 能力基本一致,区别在链路和硬件依赖。动手前先确认几件事:中控版本要 EasyClick iOS USB 10.7.0 及以上(低版本没有这套 API);手机系统建议 iOS 17 以上,16 及以下的部分机型可能走不通,可用蓝牙或 OTG 替代;数据线插好、手机上点“信任此电脑”、中控里确认已启动桥接;线一定用原装或 MFi 认证的,杂牌线很多只能充电不能传数据,是新手最常踩的坑。USB 设备授权和自动化环境卡住,可以直接对照官方文档:https://ieasyclick.com/iosdocs/advance/ai-agent/prerequisites
五个 API 上手
所有 USB HID 操作都挂在 usbHidEvent 这个对象下面。先看一个能跑的最小示例:
function main() {
// 1. 开启会话
let r = usbHidEvent.sessionStart(true);
if (!(r == null || r === "")) {
logw("开启会话失败: " + r);
return;
}
// 2. 设置屏幕尺寸,坐标换算依赖这个参数
r = usbHidEvent.setScreenSize(1170, 2532);
if (!(r == null || r === "")) {
logw("设置尺寸失败: " + r);
return;
}
// 3. 点击坐标 (300, 400)
r = usbHidEvent.clickPoint(300, 400);
logd("点击结果: " + (r == null || r === "" ? "成功" : r));
// 4. 输入文字,走剪贴板粘贴,中英文通用
r = usbHidEvent.inputText("Hello 自动化");
logd("输入结果: " + (r == null || r === "" ? "成功" : r));
// 5. 关闭会话
usbHidEvent.sessionStop();
}
main();
看代码应该注意到了,每个函数都在判断 r == null || r === ""。这是这套 API 的统一约定:返回值是 null 或空字符串表示成功,返回其它字符串就是错误信息,所以写判断不需要 try-catch,直接判断返回值就行。建议封装一个工具函数:
function _ok(r) {
return r == null || r === "";
}
sessionStart(gate) 的 gate 默认 true,作用是尝试增强兼容模式,低版本系统会自动忽略;会话已存在时会复用,需要彻底重置(断流、触摸失效)时用 sessionRestart,它等价于先 sessionStop 再 sessionStart。坐标上,setScreenSize(w, h) 设的是屏幕像素宽高,脚本坐标、投屏坐标、截图坐标三者统一,投屏里量出来的值可直接写进脚本;但分辨率变化或横竖屏切换后必须重调,否则坐标整体偏移,点歪了先查它有没有设对。
完整函数清单与 HTTP 接口
下面按用途分类整理,方便速查,每个函数的完整参数和示例以官方手册为准:https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/
会话管理:
| 函数 | 说明 |
|---|---|
sessionStart(gate) |
开启会话,已存在则复用 |
sessionStop() |
关闭会话,释放资源 |
sessionRestart(gate) |
强制重建会话,用于排障 |
屏幕与坐标:
| 函数 | 说明 |
|---|---|
setScreenSize(w, h) |
设置屏幕像素宽高 |
触控操作:
| 函数 | 说明 |
|---|---|
clickPoint(x, y) |
单击 |
doubleClickPoint(x, y) |
双击 |
press(x, y, delay) |
长按,delay 为按住毫秒数 |
swipeToPoint(x1, y1, x2, y2, duration) |
从起点滑动到终点 |
touchDown(x, y) |
按下 |
touchMove(x, y) |
移动 |
touchUp(x, y) |
抬起 |
multiTouch(points, timeout) |
按轨迹回放多点触摸 |
multiTouch 的轨迹点格式里,action 中 0 表示按下、1 表示抬起、2 表示移动,delay 是该点的延迟毫秒数:
let trace = [
{"action": 0, "x": 100, "y": 500, "delay": 20},
{"action": 2, "x": 100, "y": 300, "delay": 30},
{"action": 1, "x": 100, "y": 300, "delay": 20}
];
usbHidEvent.multiTouch(trace, 10000);
它适合做画圈、双指缩放这类复杂手势。文字输入:
| 函数 | 行为 | 使用建议 |
|---|---|---|
inputText(text) |
统一走剪贴板粘贴 | 默认用这个,中英文都稳 |
typeText(text) |
可打印英文走键盘逐键,含中文或 emoji 自动改为粘贴 | 需要模拟真实打字时用 |
setClipboard(text) |
只写入剪贴板,不粘贴 | 配合组合键手动触发粘贴时用 |
getClipboard() |
读取剪贴板 | 有已知限制,见踩坑部分 |
按键操作:
| 函数 | 说明 |
|---|---|
keyPressChar(prefix, code) |
字符按键或组合键 |
keyPress(key) |
按下单个键 |
keyUp() |
抬起全部按键 |
systemKey(key) |
系统按键,支持 home、recents、lock |
组合键的 prefix 可以是 alt、ctrl、gui、shift、r_ctrl、r_shift,不需要组合时传空字符串,gui 对应 iOS 上的 Command 键,模拟粘贴的写法是:
usbHidEvent.keyPressChar("gui", "v");
音量控制:
| 函数 | 说明 |
|---|---|
volumeUp() |
音量加 |
volumeDown() |
音量减 |
mute() |
静音 |
不写 JavaScript,也可以用其它语言通过 HTTP 对接中控。所有接口都是 POST、请求体是 JSON,地址前缀默认 http://127.0.0.1:8019,路径与脚本函数一一对应,完整的请求参数、返回字段和 Python / Node.js / cURL / C# 四种示例见官方文档:https://ieasyclick.com/iosdocs/advance/openapi/usbhid
| 脚本函数 | HTTP 路径 |
|---|---|
sessionStart |
/openapi/usbhidSessionStart |
sessionStop |
/openapi/usbhidSessionStop |
sessionRestart |
/openapi/usbhidSessionRestart |
setScreenSize |
/openapi/usbhidSetScreenSize |
clickPoint |
/openapi/usbhidClickPoint |
doubleClickPoint |
/openapi/usbhidDoubleClickPoint |
press |
/openapi/usbhidPress |
swipeToPoint |
/openapi/usbhidSwipeToPoint |
touchDown/Move/Up |
/openapi/usbhidTouchDown 等 |
multiTouch |
/openapi/usbhidMultiTouch |
inputText |
/openapi/usbhidInputText |
typeText |
/openapi/usbhidTypeText |
systemKey |
/openapi/usbhidSystemKey |
keyPressChar |
/openapi/usbhidKeyPressChar |
keyPress/keyUp |
/openapi/usbhidKeyPress、/openapi/usbhidKeyUp |
volumeUp/Down/mute |
/openapi/usbhidVolumeUp 等 |
setClipboard |
/openapi/usbhidSetClipboard |
getClipboard |
/openapi/usbhidGetClipboard |
HTTP 接口返回的是 JSON:
{
"code": 0,
"msg": "",
"data": ""
}
code 为 0 表示成功,非 0 时 msg 是错误信息。
import requests
BASE = "http://127.0.0.1:8019"
DEVICE_ID = "你的设备 ID" # 从设备列表接口获取
def call(path, body=None):
r = requests.post(f"{BASE}{path}", json=body or {}, timeout=30)
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"{path} 失败: {data.get('msg')}")
return data
# 开会话
call("/openapi/usbhidSessionStart", {"deviceId": DEVICE_ID, "gate": True})
# 设置屏幕尺寸
call("/openapi/usbhidSetScreenSize", {"deviceId": DEVICE_ID, "w": 1170, "h": 2532})
# 点击
call("/openapi/usbhidClickPoint", {"deviceId": DEVICE_ID, "x": 300, "y": 400})
# 输入文字
call("/openapi/usbhidInputText", {"deviceId": DEVICE_ID, "text": "Hello"})
# 关闭会话
call("/openapi/usbhidSessionStop", {"deviceId": DEVICE_ID})
这种方式适合把自动化能力接进已有的 Python 系统,比如电商后台、测试平台、运维工单。
一个完整流程,和踩过的坑
假设需求是:打开某个 App,等页面加载完成,点击一个按钮,输入内容并提交,然后回到桌面。
function _ok(r) {
return r == null || r === "";
}
// 随机等待,避免机械节奏
function randSleep(minSec, maxSec) {
let ms = (minSec + Math.random() * (maxSec - minSec)) * 1000;
sleep(parseInt(ms));
}
function main() {
let r = usbHidEvent.sessionStart(true);
if (!_ok(r)) {
logw("会话开启失败: " + r);
return;
}
// 屏幕尺寸要与当前实际分辨率一致
r = usbHidEvent.setScreenSize(1170, 2532);
if (!_ok(r)) {
logw("设置尺寸失败: " + r);
return;
}
// 步骤一:回到桌面,确保起点一致
usbHidEvent.systemKey("home");
randSleep(1, 2);
// 步骤二:打开目标 App(假设图标在第一屏第二个位置)
r = usbHidEvent.clickPoint(400, 780);
if (!_ok(r)) {
logw("打开 App 失败: " + r);
return;
}
randSleep(4, 7); // 等页面加载
// 步骤三:点击输入框
r = usbHidEvent.clickPoint(585, 1200);
if (!_ok(r)) {
logw("点击输入框失败: " + r);
return;
}
randSleep(1, 2);
// 步骤四:输入内容
r = usbHidEvent.inputText("这是自动化写入的内容");
if (!_ok(r)) {
logw("输入失败: " + r);
return;
}
randSleep(1, 2);
// 步骤五:点击提交按钮
r = usbHidEvent.clickPoint(585, 1600);
if (!_ok(r)) {
logw("提交失败: " + r);
return;
}
randSleep(2, 4);
// 步骤六:回到桌面
usbHidEvent.systemKey("home");
usbHidEvent.sessionStop();
logd("流程执行完成");
}
main();
三点说明:每步都判断返回值,某一步失败后继续就是在错误前提下跑,早失败早退出、日志也更好定位;等待时间要随机,固定间隔本身就是机器特征;坐标靠投屏截图量,别照搬别人脚本里的数字,机型不同坐标系就不同。
踩过的坑按频率排。坐标整体偏移,多是没调 setScreenSize,或横竖屏切换后忘了重调,还有个隐蔽原因是机型不统一——一套坐标在 iPhone 11 上对、换到 iPhone 12 就偏,批量场景建议用同型号。设备频繁掉线,按顺序排查:换原装线、USB 口换主板直出的、集线器用带独立电源的、单个集线器负载不超过 7-8 台。粘贴英文多出空格,是 iOS 键盘设置问题,到设置 → 通用 → 键盘关掉“智能标点”即可。getClipboard 有已知限制:setClipboard 写入后马上读通常没问题,但读手机上手动长按复制的内容,在部分 iOS 版本(如 26.x)会超时甚至读不到,严重时卡住剪贴板服务、需重启手机,别依赖它读人手复制的内容。系统半夜自动更新也会让脚本失效,跑自动化的设备一定要关掉自动更新。
最后是选型。USB HID 适合快速验证和单机场景,一根线就能跑,缺点是必须连着电脑;蓝牙 BLE HID 需要一块 ESP32C3 开发板,几十块钱,独特优势是能配合无自动化截图模式、整个链路不走屏幕镜像,代价是投屏帧率不高、配置更繁琐;OTG HID 用 ESP32S3 直连手机,可脱离电脑运行。先用起来建议从 USB HID 入门,跑通再按需升级。后两条链路的文档官方站都有:蓝牙 BLE 看 https://ieasyclick.com/iosdocs/funcs/ble-event-api ,OTG HID 看 https://ieasyclick.com/iosdocs/funcs/otg-event-api 。三条路线怎么取舍,站内有一篇横向对比,看那张表比看文字快:三条 HID 路线横向对比。
把要点捋一遍。USB HID 的核心思路是让电脑冒充 HID 设备、通过系统输入通道注入触摸事件,iOS 无法区分真假,所以不需越狱也不需签名,这也让它成了苹果群控里最省事的免越狱链路——不用买蓝牙板或 OTG 板,一根数据线就能跑通,脚本还能在多台设备上复用。API 层面记住三件事:会话先开、屏幕尺寸设对、返回值 null 或空字符串表示成功。排查顺序基本固定:先看物理连接,再看参数设置,最后才怀疑代码逻辑。
文中涉及的接口和配置,官方文档都有完整说明:
- EasyClick iOS USB 版总文档:https://ieasyclick.com/iosdocs/
- USB HID 函数手册(
usbHidEvent):https://ieasyclick.com/iosdocs/funcs/usb-hid-event-api/ - USB HID 开放接口(HTTP,含多语言示例):https://ieasyclick.com/iosdocs/advance/openapi/usbhid
- 蓝牙 BLE 函数:https://ieasyclick.com/iosdocs/funcs/ble-event-api
- OTG HID 函数:https://ieasyclick.com/iosdocs/funcs/otg-event-api
- 环境与设备授权:https://ieasyclick.com/iosdocs/advance/ai-agent/prerequisites
- 苹果群控系统(多机批量管理):https://ieasyclick.com/apple_qunkong/
站内也写过几篇相关的,视角不同,可以对着看:
- USB HID 如何重塑苹果群控格局:讲这条链路带来的格局变化,不是代码层面的
- 三条 HID 路线横向对比:USB、蓝牙、OTG 的成本与能力边界
- 蓝牙 HID 免签名方案 和 OTG HID 配置:另两条链路的落地细节
本文基于 EasyClick iOS USB 版开发文档整理,代码片段可直接在中控脚本编辑器中运行。
关于 EasyClick:手机自动化 AI 智能体平台,覆盖安卓免 root、iOS 免越狱、鸿蒙 Next 三大生态,提供脚本开发、苹果群控、本地中控投屏与云控系统。→ 了解全部产品
想要真实跑起来?
本文介绍的方案均可在 EasyClick 手机自动化平台落地。官网提供完整文档、开发工具与群控云控产品,免费体验。