时长:50-60 分钟
4.1 功能概述
4.1.1 快捷登录的业务场景
- 内部测试同学在日常回归、联调中,需要频繁在 测试环境 App 中登录、切换账号:
- 手工输入手机号 + 验证码重复度极高,效率低
- 不同环境(test / stage / prod-like)之间切换麻烦
- 目标:在 PC 端工具上一键输入手机号,即可远程驱动 iOS 测试版 App 自动完成登录。
4.1.2 用户体验提升
- 对测试同学:
- 从“打开手机 → 手输手机号 → 等验证码 → 手点登录”简化为“PC 上点一次按钮”
- 可以快速复现问题、切账号、切环境,极大减少机械操作时间
- 对开发同学:
- 可以更方便地在不同测试账号间切换,验证特定账号下的边界场景
4.1.3 技术实现难点
- iOS 对于自动化和 URL Scheme 调用有较多限制:
- WDA 运行在 UI Test Bundle 环境下,对自定义 Scheme 有权限限制
- iOS 15+ 对“从后台唤起 App”的行为管控更严格
- 应用需要显式声明可查询的 URL Scheme(
LSApplicationQueriesSchemes)
- 跨端通信链路复杂:
- PC 工具 → go-ios/WDA → iOS 设备 → 测试 App
- 或 PC 工具 → go-ios Tunnel + Port Forward → iOS Companion App → 测试 App
4.2 URL Scheme 机制
4.2.1 iOS URL Scheme 原理
- 每个 iOS 应用可以在
Info.plist中声明一个或多个自定义 URL Scheme: - 例如:
cardloanTest://、myapp:// - 形如浏览器 URL,但协议头由应用自定义
- 当系统收到一个 URL 请求(例如 Safari / 其它 App 调用):
- 会根据 Scheme 找到已注册的应用
- 将 URL 交给目标应用处理(
application(_:open:options:)回调)
4.2.2 自定义 Scheme 注册
- 在测试版 App 的
Info.plist中添加 URL Types: CFBundleURLSchemes包含cardloanTest- 这样系统即可识别
cardloanTest://...并路由到测试版 App
4.2.3 cardloanTest:// 协议设计
- 约定统一入口路径:
cardloanTest://www.xxxxx.com/app/operation/testEnvAutoLogin
- 通过
params参数传递 JSON:
cardloanTest://www.xxxx.com/app/operation/testEnvAutoLogin?params={...}
在 PC 端构造 URL 的核心代码(节选):
// src/utils/ios.js
const params = encodeURIComponent(JSON.stringify({
manualInputPhone: phone
}))
const url = `cardloanTest://www.xxxxx.com/app/operation/testEnvAutoLogin?params=${params}`
4.2.4 参数传递与编码
- 为什么要 JSON + URL 编码:
- 业务参数后续可能扩展(环境信息、来源标记等)
- 使用 JSON 可以灵活增加字段
- 通过
encodeURIComponent保证特殊字符在 URL 中安全传输 - iOS 端在收到 URL 后:
- 从 Query 中取出
params字符串 - 先
removingPercentEncoding,再 JSON 解析 - 根据
manualInputPhone等字段,自动填充并触发登录流程。
4.3 两种实现方式对比
本期重点对比两条链路:
- 方式一:WDA (WebDriverAgent) 方式
- 方式二:Companion 服务方式(推荐)
4.3.1 方式一:WDA (WebDriverAgent) 方式
WDA 架构简介
- WebDriverAgent 是基于 XCTest 的 iOS 自动化框架:
- 以 UI Test Bundle 形式运行在设备上
- 通过 HTTP API 接收命令(如点击、输入、打开 URL)
- 在本项目中,PC 通过 go-ios / WDA 与 iOS 设备建立连接:
- PC 工具调用
openURLViaWDA(url) - WDA 在设备上模拟“Safari 打开 URL Scheme”或直接调起对应 App
openURLViaWDA() 实现要点
- 通过 HTTP 调用 WDA 提供的 endpoint:
- 检查 WDA 服务状态(
/status) - 创建 Session / 复用已有 Session
- 调用自定义的“打开 URL”动作(内部可以是 Safari + 地址栏输入 + 回车)
- 在
openXiaoyingTestLogin()中调用链条大致为:
const url = buildCardloanTestUrl(phone)
const wdaStartTime = Date.now()
const wdaResult = await this.openURLViaWDA(url)
const wdaDuration = Date.now() - wdaStartTime
并配合 handleOpenSchemeAlert() 自动点击“是否打开小赢卡贷”的系统弹框。
WDA 方式的优势与局限
- 优势:
- 早期实现成本较低,复用现有 WDA 自动化能力
- 对 App 侵入小,主要在 PC 端和 WDA 侧做工作
- 局限:
- 依赖 UI Test Bundle,对自定义 Scheme 权限有限制:
- iOS 15+ 部分场景会报“UI Test Bundle 无法打开 scheme”
- 弹框处理复杂,需要额外逻辑自动点击“打开”
- WDA 自身稳定性受 XCTest 生态影响(易受系统版本、证书等影响)
适用场景
- 已经在团队内大量使用 WDA 进行 UI 自动化,并且:
- 目标设备系统版本相对稳定
- 对弹框处理、证书续期等维护成本可接受
4.3.2 方式二:Companion 服务方式 ⭐(推荐)
PhotoCompanion 服务架构
- 在 iOS 设备上运行一个 Companion App(如 PhotoCompanion):
- 使用 Swift 编写,内部集成
WKWebView或直接使用UIApplication打开 URL - 监听固定端口(如 12345),接受来自 PC 的 TCP 请求
- PC 侧通过
go-ios建立隧道 + 端口转发: ios tunnel start/forward等命令- 然后通过
URLService封装成易用的 API。
Swift NWConnection 通信
- Companion App 使用
NWListener+NWConnection: - 在本地端口(如 12345)监听连接
- 每个连接接受一条指令(如
AUTO_LOGIN),解析 JSON 请求 - 执行对应动作(构造 URL Scheme、检查前台状态、打开 App)
handleAutoLogin() 实现思路
在 ServiceBinary.swift 中(示意):
func handleAutoLogin(_ request: AutoLoginRequest) -> AutoLoginResponse {
// 1. 校验参数(手机号、环境等)
guard !request.phone.isEmpty else {
return .error(code: .invalidPhone, message: "手机号为空")
}
// 2. 构造 URL
let params: [String: Any] = ["manualInputPhone": request.phone]
let jsonData = try? JSONSerialization.data(withJSONObject: params)
let jsonString = jsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
let encodedParams = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let urlString = "cardloanTest://www.xiaoying.com/app/operation/testEnvAutoLogin?params=(encodedParams)"
// 3. 检查当前前台 App 状态
guard UIApplication.shared.applicationState == .active else {
return .error(code: .notForeground, message: "Companion 未在前台运行")
}
// 4. 打开 URL
if let url = URL(string: urlString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
return .success(message: "AUTO_LOGIN dispatched")
} else {
return .error(code: .invalidURL, message: "URL 构造失败")
}
}
前台状态检查机制
- 关键点:Companion 必须在前台 才能可靠地调起其它 App:
- 通过
UIApplication.shared.applicationState == .active检查 - 不满足时直接返回错误给 PC,提示用户:
- “请将 PhotoCompanion 切换到前台后重试”
LSApplicationQueriesSchemes 配置
- iOS 为防止滥用,要求在
Info.plist中显式声明可以查询/打开的 scheme: - 在 Companion App 和测试版 App 中分别配置:
LSApplicationQueriesSchemes包含cardloanTest等- 否则通过
canOpenURL检查时会失败。
方式二的优点
- 不依赖 UI Test Bundle,受系统限制更少
- 协议完全由我们自己定义和演进,调试体验更好(可在 Xcode 控制台观察日志)
- 通过 Swift 实现,能更好地利用 iOS 平台特性(例如前台状态检查、App 内部路由)。
4.4 完整登录流程
整体链路可以概括为:
用户输入手机号
→ 数据银行树获取手机号(可选)
→ 自动填充手机号
→ 点击登录按钮
→ 检查 App 是否安装
→ 构建 URL Scheme
→ 优先尝试 WDA 方式
→ 失败则回退到 Companion 方式
→ iOS 端接收并打开 URL
→ 应用自动登录
→ 返回执行结果
4.4.1 PC 前端侧流程(以 openTestLogin 为例)
// src/utils/ios.js(节选,伪代码化说明)
async openTestLogin(phone, deviceId = '') {
// 1. 检查测试版 App 是否安装
const check = await this.isAppInstalled('com.xxxying.beijing.cashloan', deviceId)
if (!check.success || !check.installed) {
return { success: false, error: '未安装测试版 App,已取消跳转' }
}
// 2. 根据 iOS 版本决定是否先尝试 WDA
const iosVersion = await this.getIOSMajorVersion()
const isIOS15Or16 = iosVersion === 15 || iosVersion === 16
let wdaResult = { success: false, error: '', details: '' }
if (!isIOS15Or16) {
// iOS 17+:优先 WDA
const url = buildCardloanTestUrl(phone)
wdaResult = await this.openURLViaWDA(url)
if (wdaResult.success) {
// 异步处理“是否打开”弹框
await this.handleOpenSchemeAlert(...)
return { success: true, method: 'wda', message: '已通过 WDA 在设备上打开 URL' }
}
}
// 3. 回退/直接使用 Companion AUTO_LOGIN
const companionResult = await this.openLoginViaCompanion(phone, deviceId, wdaResult)
return companionResult
}
4.4.2 iOS 端流程(Companion + 测试 App)
- Companion App 通过
NWListener接收 AUTO_LOGIN 请求 - 解析手机号/环境参数,构造
cardloanTest://...URL - 检查自己是否在前台、目标 App 是否可打开
- 调用
UIApplication.shared.open(url)打开测试版 App - 测试版 App 在
openURL回调中解析参数,自动填充手机号并触发登录。
4.5 关键技术点
4.5.1 App 安装检测:isAppInstalled()
- PC 端通过 go-ios / WDA 或 Companion 提供的能力检测:
- 是否已安装目标 Bundle ID(
com.xiaoying.beijing.cashloan) - 未安装时直接在 PC 工具上提示,避免后续“什么都没发生”的困惑。
4.5.2 前台状态检查:applicationState == .active
- Companion App 中通过:
guard UIApplication.shared.applicationState == .active else {
return .error(code: .notForeground, message: "Companion 未在前台运行")
}
- 确保:
- 只有在 Companion 在前台时才允许发起 AUTO_LOGIN
- PC 端能拿到明确的错误原因,给用户友好提示。
4.5.3 错误处理与超时机制
- 错误分类:
- App 未安装:提示安装测试版 App
- WDA 不可用 / 超时:提示检查 WDA 配置或直接依赖 Companion
- Companion 不在前台:提示“请将 PhotoCompanion 切换到前台后重试”
- 超时处理:
- 对 WDA 调用设置超时时间,超时后自动回退到 Companion 方案
- 避免长时间卡住导致用户误以为工具“死了”。
4.5.4 日志追踪
- 前后端统一使用结构化日志:
[openTestLogin] 步骤 x/5: ...[URLService] Step x: ...- Companion 端在 Xcode 控制台打印关键节点
- 出问题时,可根据日志快速定位:
- 卡在 WDA 还是 Companion?
- 是端口转发问题,还是 iOS 前台状态问题?
4.6 代码实现详解
4.6.1 openTestLogin() 函数解析
- 主要职责:
- 校验输入手机号
- 检查测试版 App 是否安装
- 根据系统版本选择 WDA / Companion 流程
- 将所有错误/提示信息统一返回给前端组件,用于 UI 提示。
4.6.2 ServiceBinary.swift 中的 handleAutoLogin() 实现
- 负责处理 AUTO_LOGIN 指令:
- 从 TCP 数据中解析请求 JSON
- 校验参数、构造 URL、检查前台状态
- 调用
UIApplication.shared.open打开测试版 App - 返回标准化的响应结构(
status、message、errorCode等)。
4.6.3 参数构建与 URL 编码
- 约定所有业务参数都放在
paramsJSON 中: - 便于扩展与调试
- 便于在 App 端统一解析和路由
- 注意点:
- JSON 编码错误 / URL 编码错误都会导致最终 URL 无法解析
- 建议在两端都加入必要的校验和日志输出。
4.6.4 错误码定义与处理
- 在 Companion 与 PC 端之间约定错误码,例如:
INVALID_PHONENOT_FOREGROUNDAPP_NOT_INSTALLEDURL_BUILD_FAILED- PC 工具根据错误码:
- 映射为适合测试同学的中文提示
- 记录原始错误码以便开发排查。
4.7 调试与排障
4.7.1 常见问题排查
- 现象:点击快捷登录后,设备无反应
- 检查测试版 App 是否已安装
- 检查 Companion 是否在前台运行
- 查看 WDA / Companion 日志,确认 URL 是否正确构造
- 现象:出现系统弹框但没有自动点击“打开”
- 检查
handleOpenSchemeAlert()是否正常执行 - 检查 WDA
/status、/wda/sessions接口是否可用
4.7.2 日志分析方法
- PC 端:
- 查看工具内日志区域,搜索
openTestLogin/URLService - 重点关注“步骤 x/5”打印,确认流程卡在哪一步
- iOS 端:
- 使用 Xcode 连接设备,查看 Console 输出
- 搜索
AUTO_LOGIN/Listener ready on port: 12345等关键字。
4.7.3 测试技巧
- 优先在低风险环境(如 test)验证新版本登录流程
- 为不同错误场景构造专门的测试用例:
- 未安装 App
- Companion 在后台
- iOS 不同版本(15/16/17+)
- 建议在 UI 中增加“复制调试信息”按钮,方便测试同学将完整上下文反馈给开发。
> 本期从业务场景出发,拆解了 iOS 快捷登录从 URL Scheme 设计、WDA 调用到 Companion 服务的完整链路, > 帮助大家理解“为什么需要两套方案”、“各自的优缺点”以及“出现问题时该怎么看日志、怎么排查”。







Comments | NOTHING