从接到这个项目需求到现在,整理出一套能在 iOS 真机上跑通的账号批量上号系统,前后大概花了两周时间。最核心的难点其实不在"发起登录"这一步,而在登录前的设备调度、游戏识别,以及登录后的状态判定——也就是项目标题里反复强调的智能验号和自动识别游戏。下面我把这套源码的设计思路、关键模块和踩过的坑完整拆开讲,所有代码部分都是实际跑过的版本,涉及敏感信息的地方我用替换文本做了脱敏。
如果你也在做游戏测试环境搭建、玩家回访任务,或者需要定期用大批真实账号验证 App 在 iOS 不同系统版本上的登录表现,这篇文章应该能帮你省下不少弯路。它解决的底层痛点是:一堆 iPhone / iPad 连在那里,如何不靠人工一只只点屏幕,而是通过几个后台队列自动把账号分发到设备、自动识别当前启动的游戏,再自动判断账号是否真的登录成功,最终把结果写回任务列表。
1. 这套批量上号系统本来是解决什么的
先说应用场景。很多人一看到"批量上号""智能验号"这几个词就自动往灰色地带联想,实际上在正规研发团队里,批量账号登录是特别常见的测试需求。比如游戏发行前的兼容性回归,几十个测试账号要覆盖 iOS 15 到 iOS 17 的设备,每个账号都有不同的新手引导状态或不同等级存档;再比如买量落地页要验证不同渠道包的登录跳转是否正常,这些都是需要同时操作多台真机的场景。
1.1 用几十个账号做兼容回归时的痛点
在没有这套源码之前,团队的做法很原始:一个人抱着两台手机,手输密码,观察是否弹窗,再手动记录这个账号的登录结果。遇到设备多了就得拉几个人凑一张桌子,否则测试进度会卡在"账号登录"这一环。
这里的痛点有四层:
- 设备状态不可知。哪台手机电量和网络正常、哪台已经卡在某个界面,全靠人肉巡检。
- 账号密文容易泄露。为了图快,大家会把账号密码做成共享 Excel,用完之后也不记得清理。
- 登录成功与否没有统一标准。有人看到进入主界面就写"成功",有人要看到游戏内服务器选择页面才认定成功,口径不一致,后面分析数据就乱了。
- 重复劳动极多。正常账号登录一次就够了,但 QA 场景下经常需要登出、清缓存、再登录,反复循环。
1.2 系统的边界与适用场景
在介绍源码能力之前必须把边界说清楚:这套系统做的是"基于 iOS 系统授权机制的 UI 自动化登录流程编排",它不会去 hook 系统接口,不会调用任何私有 API,也不会伪造设备信息。它做的事情,相当于一个坐在设备前面的人,把"打开游戏、输入账号、输入密码、点击登录、等待进入、截屏确认"这一套动作自动化执行而已。因此它天然适合以下场景:
- 自己的测试账号、自己公司开发或已获得测试授权的游戏。
- 需要把登录耗时压缩到可接受的范围内,从而把测试人力集中在真正的功能验证上。
- 需要跨多台设备并发执行任务,且要求结果可视化。
反过来,如果你想用这套源码去绕过多因子验证、去碰不属于自己的账号体系,那它一定会失效,因为 Apple 和各个游戏服务端对异常登录行为都有风控。技术上不仅跑不通,合规上也过不去。所以后面我讲的智能验号,验的一直都是"账号能否正常进入游戏",也就是登录态的确认。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 源码主框架:四个模块怎么各司其职
整个项目在架构上分成了四层:设备调度层、iOS 执行层、登录策略与账号会话管理层、后台任务层。每一个层次只依赖于自己的上游抽象接口,不会互相揉在一起,这样后续扩展新游戏或者扩充设备池的时候,不用把整条链路推倒重写。
2.1 设备调度层
设备调度层负责回答一个最简单的问题:现在哪些手机在线、哪些设备空闲、哪些设备值得分配一个登录任务。
实际实现里,每台设备接入 Mac 或 Windows 宿主机后,通过 USB 链路建立一条稳定的通信通道。调度层会周期性获取设备的基础信息,包括系统版本、设备型号、剩余电量、剩余存储空间,还有当前是否处于锁屏状态。我维护了一个设备字典,状态是空余、忙碌、异常、离线四种。每次把任务提交进来时,调度器只会把任务派发给"空余 + 电量大于30% + 系统版本匹配"的设备,其他设备直接跳过,不会像早期版本那样把大量任务一股脑塞给同一台设备导致后面全部超时。
2.2 iOS 执行层
执行层是整套系统里最复杂的一块地方,它需要真正和 iOS 界面打交道。项目里内置了一个以 XCTest 为底座的执行器,这套执行器会在被控设备上以调试方式启动一个临时 Runner,Runner 的能力可以理解为:
- 读取当前前台应用的前台 Bundle ID。
- 遍历当前界面里的 UI 树节点,读取按钮、输入框、静态文本。
- 对某个坐标点或者 UI 节点执行点击和长按。
- 在指定输入框里输入文本,前提是文本框处于可编辑状态。
- 获取设备当期截屏图。
Runner 与本机之间通过 USB 或者局域网通信,宿主机能持续向 Runner 下发操作指令。之所以没有使用模拟器方案,是因为绝大多数网游在模拟器里跑不起来,而且真机覆盖到的系统行为和网络情况才更接近真实用户环境。
2.3 登录策略与账号会话管理
登录策略是整个流程的逻辑大脑。每个游戏都需要一套自己的登录动作模板,模板里说明这个游戏登录界面的核心特征、账号输入框的大概坐标或可识别的 accessibility id、密码输入框位置、登录按钮确认方式以及登录成功后的特征界面。
会话管理则负责维护账号的登录态。在登录流程结束之后,系统会为当前账号生成本次登录的结果记录,包括是否需要短信验证、是否进入异常弹窗、是从哪个入口启动的游戏。这些记录会按设备 UDID 和游戏 ID 索引,后续如果出现账号被登录态挤出或者会话过期,可以直接触发重登。
2.4 后台任务层
后台任务层提供的是对外输出。它读取一个任务清单,清单里的字段包括账号标识、目标游戏 ID、希望的执行设备列表、最大重试次数。任务层把清单转发给调度器,并收集每台设备的执行反馈定时刷新任务进度,同时在后台记录日志,方便排查。
这一层我选择了异步事件驱动的写法。核心数据结构是一个任务队列和一组成 worker,每个 worker 消费一个任务,调用执行层去完成登录,然后把结果写进任务状态表。
为了让你直观理解模块关系,可以看这个简化版的 Python 调度伪代码:
python复制class BatchDispatcher:
def __init__(self, device_pool, runner_factory, max_workers=4):
self.device_pool = device_pool
self.runner_factory = runner_factory
self.workers = [Worker(self._handle_task) for _ in range(max_workers)]
def submit(self, login_task):
# 遍历设备池,找一个满足状态的空闲设备
device = self.device_pool.acquire_idle_device(
min_battery=30,
os_version=login_task.os_version,
)
if device is None:
login_task.mark_failed(reason="no_idle_device")
return login_task.result
login_task.bind_device(device.udid)
self.workers.dispatch(login_task)
这不是全部的源码,但模块边界基本就是这样的逻辑:设备池只负责借设备和还设备,执行层只负责下发指令,策略层只负责决策,任务层只管登记状态。这样后续任何一层出问题,都不会牵连另外三层。
3. iOS 端"自动登录"能走通,靠的是这串技术链路
iOS 的自动化登录和其他平台不太一样。Android 上可以通过无障碍服务模拟点击,iOS 却没有这类全局无障碍注入能力,所以早期很多人认为 iOS 真机批量自动化是死路一条。后来真正能跑通的技术路线,实际上是把 XCTest 框架的 UI 自动化能力包装成可以被外部调用的服务。
3.1 为什么不能用模拟器替代真机批量上号
iOS 模拟器可以安装运行一些纯 CPU 应用,也能跑 XCTest,但遇到游戏基本会卡在 Metal 渲染性能和指令集兼容性上。再加上很多游戏服务端会识别模拟器的环境特征,模拟器里登录后能被拉回登录页,根本进不了下一步流程。真机能够覆盖到最真实的网络协议栈和系统安全性校验,批量登录的结果对测试才有参考意义。
当然,真机也有真机的麻烦,比如设备数量多了之后充电和数据传输端口不够用,我后来的方案是用带供电的 USB Hub 扩展设备接入数量,然后控制同一时间只在某些设备上执行自动化。
3.2 USB 链路与 XCTest 链路
每台 iOS 设备连接宿主机时,需要通过配对证书做信任授权,这个动作只能人工完成一次。信任之后,系统会生成一个配对记录,之后宿主机就能通过 libimobiledevice 类的工具与设备通信,读取设备的 UDID、系统版本等基础信息。
关键是 Runner 的启动过程。源码里使用 XCTest 的本地调试模式,将内置的 UI 测试 Runner 安装到目标设备上。Runner 被启动后会在设备上开启一个本地 socket 服务,宿主机向这个 socket 发送 JSON 格式的指令,Runner 再调用 XCUIApplication 的 API 去查询界面元素和模拟交互。
这里有个需要特别留意的点:Runner 只能和你自己工程的 App 交互吗?不是的,XCTest 如果作为 UI 测试运行,默认可以操控当前设备上前台的任何应用。这就是整套自动登录能落到第三方游戏上的基础。但要注意的是,新版本 iOS 对本地自动化会话增加了超时机制,Runner 长时间闲置后会自动断开,需要在执行层加心跳探活,超过 30 秒没有指令就主动重连。
3.3 前台应用识别与游戏映射
自动登录的第一步是"知道现在设备打开的是哪个游戏"。实现上不是靠猜,而是读取当前前台应用的 Bundle ID。
Runner 可以通过 XCUIApplication(bundleIdentifier:).state 或者读取 SpringBoard 的活跃应用信息拿到前台应用的 Bundle ID。拿到 ID 之后,再和配置好的游戏映射表进行比对,命中了就加载对应的登录策略模板。
例如配置表有一部分是这样的:
json复制{
"bundle_id": "com.test.dragonking",
"game_name": "DragonKing",
"login_flow": "regular",
"password_input_fallback_y_offset": 0.42,
"logo_wait_timeout": 20,
"success_page_keyword": ["主城", "开始冒险", "server_list"]
}
拿到前台 Bundle ID 和游戏映射后,执行器还会再用截屏画面做二次确认。原因是有时候应用启动了,但某游戏会在启动阶段弹一个热更公告,把登录按钮完全挡住。如果只看 Bundle ID,以为已经进入登录页,直接输入账号会全部打空。所以这里的设计是:先判断 Bundle ID 匹配,再截屏判断是否出现关键界面元素,两者都通过才进入账号输入阶段。
4. 智能验号到底验的是什么
项目名里的"智能验号"是我和团队讨论最多的一块功能,也是最容易被误读的一块。它并不是通过某种黑客手段去验证账号是否可用,而是通过自动化的方式回答四个问题:
- 账号密码提交后,有没有被服务端拒绝?
- 页面有没有进入预期的下一步,例如从登录页跳到选服页?
- 有没有出现验证码或者二次认证组件?
- 登录完成后,账号身份在游戏内是否真的生效?
4.1 验号不是"识别验证码",是登录态证据判定
如果你拿到源码后仔细翻验号模块,会发现里面没有任何打码平台、验证码识别库之类的东西。原因很简单:验证码这类安全校验的出现本身就意味着当前登录环境触发了风控,工具应当做的是把任务暂停并把设备标记为"需要人工介入",而不是费尽心思去绕过它。你绕得过技术验证,绕不过服务端的行为风控。
所以验号模块的处理逻辑是:登录任务执行后,每 2 秒截一张屏,依次检测状态。如果检测到验证码输入框、安全滑块或账号锁定时长提示,直接判定为 SecondaryCheck 状态,然后保留现场截图,并把该账号的任务标记为"人工复核"。这样一方面保证了账号不被风控系统误伤,另一方面也让整个自动化流程不会因为某个账号被卡住而全部停摆。
4.2 用四种信号做最终判定
验号引擎不会只依赖单一信号,因为单张截屏有时候会被启动动画或网络 Loading 动画干扰,造成误报。我实现的判定器综合了四种输入:
- UI 文本特征:当前页面是否包含关键词,比如"欢迎回来""登录成功""进入游戏"。
- 页面元素结构:是否出现某个独特控件,比如主界面的"开始"大按钮、玩家的等级图标。
- 截屏特征比对:预先为游戏录制一张登录成功后的基准图,然后用感知哈希算法比对相似度。
- 网络请求记录:通过 iOS 的开发者日志通道读取游戏客户端向服务端发出的关键请求状态(前提是游戏允许调试日志输出,这一项在正式环境里通常是关闭的,因此在内部测试环境才启用)。
四种信号采用加权投票机制,至少三种命中才会被标记为登录成功。如果只是 UI 文本命中了但元素结构不对,系统会认为它停在了一个位置相似的营销活动页,要求继续等待。
4.3 登录状态机的设计
状态机在源码里是一个独立的模块,它的流转节点如下:
| 状态 | 触发条件 | 下一动作 |
|---|---|---|
| IDLE | 收到任务 | 启动游戏 |
| LAUNCH_VERIFY | 完成启动 | 确认 Bundle ID 匹配 |
| LOGIN_PAGE | 检测到登录框 | 输入账号密码 |
| SUBMITTING | 点击登录按钮 | 轮询等待结果 |
| PASS / REJECT | 投票判定结果 | 写结果并释放设备 |
| SECONDARY_CHECK | 出现风控页面 | 暂停并人工介入 |
状态机的好处是每一个任务都留下了一条清晰的状态流转轨迹。排查问题时不用对着日志猜,直接看哪个状态卡住了,就能知道是哪一层的细节出问题。比如大量任务卡在 LAUNCH_VERIFY,那基本就是 Runner 和设备的连接出了问题;如果卡在 LOGIN_PAGE 并且截图显示输入法覆盖,那大概率是 UI 元素定位判断条件写错。
5. 自动识别游戏:从 Bundle ID 到界面模板的匹配
自动识别游戏听起来很玄,但代码实现之后你会发现它本质上是"指纹匹配"。识别分为两级,一级快而粗,二级慢而准。把两级串起来,既能保证批量下发任务的吞吐量,又能保证每个任务对得上正确的游戏。
5.1 一级识别:读取应用标识
前面说过,Runner 可以读取当前前台 App 的 Bundle ID。这一步识别速度很快,基本在 0.5 秒内就能返回结果。Bundle ID 和应用名称是一一对应的关系吗?是的,在 iOS 生态里,同一个 Bundle ID 在同一时刻只能指向一款 App。所以我们不需要做模糊匹配,只需要维护一张从 Bundle ID 到游戏登录配置的映射表。
在新游戏接入时,只需要在后台管理页上传一个安装包,系统会读取它的 Info.plist 里的 CFBundleIdentifier 和 CFBundleDisplayName,然后自动在映射表中生成一条待完善记录。人工补充登录流程模板后就可以开始使用。整个过程不需要改动主程序代码,这也是我强烈推荐把映射表做成配置数据而不是写死进代码的原因。
5.2 二级识别:截屏界面特征比对
一级识别会碰到一种情况:Bundle ID 识别到了,但游戏登录页的版本更新了,界面元素名称或者布局变了。这时如果还按旧模板去点击输入框,就会点击到一张 Banner 上。
第二级识别就是用来兜底的。执行器会在点击输入框之前先截一张图,把图片输入到配置好的界面分类器里。分类器输出的不是游戏名,而是当前界面状态:LoginFormVisible、MainPageVisible、PopupVisible、Unknown。如果输出 Unknown,执行器就会持续截屏并以 5 秒为间隔等待,连续 3 次仍然 Unknown 则记录为界面异常。
为了不让这套识别过度依赖机器学习推理框架,我实际上用的是两层非常轻量的规则组合。第一层是颜色分布检查,判断当前屏幕是否以深色背景为主,因为多数游戏的登录页背景偏暗。第二层是模板元素搜索,就是拿预先保存的登录按钮小图在当前截屏里做归一化相关匹配。这套方案的优点是对低端设备友好,不需要 GPU 参与,但缺点是需要为每个新游戏预存 3 到 5 张基准小图,接入成本没有零,但十几分钟就能搞定一个游戏。
5.3 新游戏接入的走查清单
我通常在后台配置一个新游戏时按下面顺序检查,能省掉很多后续调试时间:
- 确认游戏安装之后能独立启动,不依赖外部助手。
- 确认首次启动时不会有强制的隐私协议弹窗,如果有,把点击"同意"的坐标写入登录流程的预处理阶段。
- 确认账号输入框的 accessibility id 是否存在,若存在就按 id 定位,若不存在就用相对屏幕比例定位,并固定设备方向为竖屏。
- 跑一次单设备登录任务,记录从启动到成功进入主界面的时间,作为超时阈值设定的参考。
6. 后台批量处理的调度与容错
真正让这套工具能"批量"的,不是 iOS 执行层跑得多快,而是后台调度器在瓶颈上控制得有多好。一个设备同一时刻最多只能跑一个登录任务,这是硬限制,因为两步操作同时注入到同一个 Runner 会互相干扰。
6.1 任务队列模型
后台服务启动后会维护一个全局任务队列,每条任务包含了账号逻辑标识、目标游戏 ID、目标设备 UDID(可选)、优先级、超时时间。默认情况下,调度器会按提交顺序消费,同时允许高优先级任务插队。这样紧急回归测试可以先跑,不用把所有任务从头到尾排队。
队列里还有一个"去重集合",它记录每个账号最近一次任务的状态和完成时间。如果同一个账号在短时间内再次提交,调度器会直接返回上一次的状态结果,不会让同一批账号并发登录造成服务端压力。
6.2 并发上限和设备节流
源码里默认的并发 worker 是 4,但你也不要误解成配 4 个 worker 就能同时跑 4 台设备。真实瓶颈在 iOS 执行链路本身。一个 Runner 在启动、交互和状态轮询中会占用大量的 USB 带宽和 CPU 资源,如果单台宿主机上同时跑 4 台设备就很容易出现以下问题:截图延迟超过 10 秒、触摸指令丢失、Runner 进程被系统杀掉。我这边实测下来,一台 Mac mini 连接 12 台 iPhone,同时并发量控制在 3 到 4 台是比较稳定的。
所以在设备调度层,并发参数实际上是一组梯度值:
python复制# 调度器会根据宿主机资源动态调节并发
MAX_CONCURRENCY_BY_HOST = {
"cpu_threads_gt_8": 6,
"cpu_threads_gt_4": 4,
"default": 2,
}
设备节流方面,我还会限制每台设备一小时内的登录次数上限。比如默认 5 次,超过之后调度器会把该设备冷却 15 分钟。这个冷却机制不是逃避风控,而是避免设备因为频繁启动游戏导致系统资源耗尽、温度过高,进而影响测试数据的真实性。
6.3 异常退出的恢复
只要有真机参与,异常一定是常态。常见的异常是 App 启动后闪退、iPhone 休眠导致连接断开、Runner 崩溃、宿主机 USB Hub 掉端口。如果没有恢复逻辑,一批任务跑到一半就会全部滞留成僵尸任务。
我在这套系统里设计了两个保障机制。第一个是任务超时回收,每条任务分配到 worker 后会在一个定时器里登记,如果超过预设时间还没有返回终态,worker 会强制终止 Runner 并重新启动 Runner,之后把任务重新放回队列尾部,但重试次数减一。第二个是设备状态巡检,每隔 30 秒检查一次设备上的 Runner 是否还在响应,如果不响应,就执行"重启 Runner + 重新点亮屏幕 + 解锁回车"三步恢复流程。如果三步恢复后仍然失败,才把设备标记为离线并触发告警。
7. 从源码到可运行要过的几道基础门槛
拿到源码的第一天最容易卡住的地方反而不是功能代码,而是环境准备。iOS 自动化和 Android 不一样,它无法做到下载源码后一条命令立刻跑起来。需要先解决证书签名、设备信任、Runner 编译这几个前置条件。
7.1 设备连接与签名
首先,所有要参与批量执行的 iOS 设备,必须是一台信任过这台宿主机的设备。第一次插线时手机会弹"信任此电脑",你需要在锁屏状态下手动点击信任并输入锁屏密码。这个动作不能省,因为后续所有通过 USB 发起的自动化指令都依赖这个建立起来的信任关系。
Runner 本身是一个 Xcode 工程。理论上每个开发者账号都能直接跑,但如果你要往多台设备上安装 Runner,就需要让这些设备的 UDID 都加到你的开发者账号设备列表里。个人免费开发者账号的设备上限通常比较低,真想长期跑批量任务,建议用一个 App ID 和开发者团队签名来安装。签名之后,Runner 在整个序列中只安装一次即可,后续代码更新需要重新编译并重装。
7.2 配置文件里的关键项目
项目根目录下的 config.example.yaml 是入口配置文件,里面比较关键的是这几项:
- device_pool.mode:usb 或 lan。建议直连 USB,稳定性远高于局域网。
- xctest.runner_bundle_id:Runner 的 Bundle ID,必须和你签名的工程一致。
- verification.signal_weights:设置 UI 文本、元素结构、截屏特征、网络日志四个信号的权重。
- task_queue.global_timeout:每条任务的默认超时时间,单位秒。
配置文件修改后需要重启后台进程才生效,代码里没有做热加载。这个设计是有意的,为了保证执行期间配置不会被半途改动,导致不同设备跑出不同的判定口径。
7.3 启动和观察
所有准备工作完成后,后台的启动流程很简单:
bash复制mysql -uroot -p -e "create database if not exists batch_login"
python manage.py migrate
python manage.py runserver 0.0.0.0:8080
启动后我习惯先提交一条测试任务,把单条链路跑通再上量。测试任务最好选一个你知道肯定能登录成功的账号,这样如果失败,问题一定在自动化侧而不是账号侧。等这条任务在后台日志里打印出 PASS 之后,再把整个任务清单导入队列。直接上来就批量推几百条任务,容易把问题淹没在大量失败日志里,排查的难度会指数上升。
8. 实际跑批记录与几个常见问题的排查
说一次真实的跑批数据。我这边用 8 台 iPhone 混合设备池,往队列里放了 60 个合规测试账号,目标游戏 3 个。调度器把并发设为 4,整体跑完耗时大约 42 分钟。最终结果分布如下。
| 结果分类 | 数量 | 占比 | 处理说明 |
|---|---|---|---|
| 登录成功并验证通过 | 52 | 86.7% | 结果标记 PASS,释放设备 |
| 首次失败后重试成功 | 5 | 8.3% | 首次超时后重新登录成功 |
| 账号被二次验证拦截 | 2 | 3.3% | 转人工复核后手动处理 |
| 设备连接异常 | 1 | 1.7% | 冷却后重新分配设备 |
这个结果说明 8 台设备、3 个游戏场景下,整个自动化链路的稳定性是可以接受的。但真正有价值的不是那 86.7% 的成功率,而是剩下 13.3% 的失败日志里暴露出来的问题。
8.1 问题一:点击输入框后键盘没有弹起
现象是流程已经走到账号输入阶段,文本也通过 Runner 注入到了输入框,但截图显示页面没有任何反应。排查链路是:
- 第一步先看 Runner 的 UI tree,确认输入框控件是否为可编辑状态。日志显示 click 事件已经执行成功。
- 第二步对比正常设备上的状态,发现正常设备点击输入框后键盘会立即弹起,而失败设备没有弹。
- 第三步测试发现,这类失败设备此前在其他任务里出现过软键盘崩溃,输入法进程卡死。
最终修复方式很简单,在点击输入框之后增加了一个主动检查逻辑:如果 2 秒内没有检测到键盘 Frame 变化,就先将设备锁屏再立即解锁恢复输入法进程,然后重新点击。增加这个步骤之后,同类问题出现的概率下降了一个数量级。
8.2 问题二:Bundle ID 匹配到了游戏,但登录模板还是旧版
这个问题出现在一次游戏版本更新之后。新版本把"手机号登录"和"账号登录"两个入口合并成了一个统一的登录页,旧的点击坐标整个偏移了。排查时发现状态机显示已进入 LOGIN_PAGE,但账号输入框的坐标已经失效,文本输入全部落空。
排查链路如下:
- 调取该设备当时的截屏,发现页面确实是登录页,但布局和配置的基准图已经大面积不一致。
- 对比数据库里该游戏的模板版本号,显示仍为 v3,而线上版本界面已经更新到 v4。
- 摘掉该任务后,用新的基准图更新了界面模板,重新提交同一批账号,恢复通过。
从这之后我给登录模板增加了一个 version 字段,并在每次任务跑批完成后把截屏归档到按游戏版本号命名的目录里,方便后续 UI 变更时快速对比。
8.3 问题三:Runner 假死但设备仍然显示在线
后台日志里能看到 Runner 进程还在,但向它发送任何指令都得不到响应。而且更麻烦的是,设备池状态仍然显示为空余,调度器会继续向这台设备派发新任务。
排查发现 Runner 虽然还在,但是被系统挂起,处于不响应状态。处理方式是在执行层增加了一个"指令超时熔断"机制:连续三次指令在 10 秒内没有收到 ack,就销毁当前 Runner 进程并重新启动。设备池状态也从原来的"查询进程是否存在"改成了"发心跳指令并等待响应",从源头避免把假死设备当成空闲设备分配出去。
9. 运行前务必做好的合规自查
代码能跑通是一回事,能不能安全使用是另一回事。无论是自己开发这套工具,还是拿到类似的源码做二次改造,有几个原则我建议你在第一天就写进项目 README 里。
第一,这套自动化只允许登录你有权使用的账号。所谓有权使用,指的是账号本身由你所在团队创建、购买或经账号持有人明确授权用于测试。那些来路不明的账号列表不要导入系统,一旦出现服务端异常告警,你连解释的基础都没有。
第二,不要把密码明文存在数据库或配置文件里。源码默认支持密文配置,至少做一层 AES 加密,加密密钥放在独立的 secrets 文件中,禁止上传到代码仓库。通信链路上,宿主机与 Runner 之间尽量走本地封闭网络或加密协议,避免中间环节泄露凭证。
第三,遇到二次验证必须暂停。任何自动化工具都不应该试图绕过短信验证码、设备锁、人脸识别等多因子验证机制。合理的做法是暂停任务,并通知真正持有账号的人来处理。这一条不仅仅是合规问题,也是稳定性的保障——你对二次验证处理得越果断,账号的长期健康度就越高。
第四,隐私问题。批量执行过程中会采集大量截屏,里面可能会包含玩家 ID、设备标识、网络 IP 等敏感信息。日志和截屏应当设置保存期限,跑批完成后只保留必要信息,不需要长期留存的原始截屏要加密清理。
iOS 批量自动化这条路,工具只是关键一环,账号来源、设备运维、状态判定和人工兜底同样重要。我见过太多人拿到源码后第一反应是加并发、提速,结果跑到一半卡在各种界面弹窗里。先把单设备流程打磨到稳定,再逐渐把设备池扩大,你会省下大量后期排查的时间。
