随着业务的扩张,单个 App 的功能越来越多,工程复杂度越来越高,每天MR可达上百次,代码变更可达上千处,航母级的 App 在这一点上更为严重。如何在频繁的代码变更中保障App质量,成了各个业务的痛点。靠传统的人工测试已无法满足各业务的需求,我们需要将更多的测试场景自动化。
自动化测试需要将人工交互行为变成自动化的原子操作。比如应用安装卸载、屏幕点拖拽及缩放、实体按键点击、设备信息获取、应用启停等等。这就需要一款工具来驱动 iOS 设备完成以上操作。这篇文章主要介绍字节 iOS 自动化测试驱动工具 bdc 的探索过程及实现原理。
在介绍 bdc 的探索过程及实现原理之前,先介绍一下 bdc 的能力:
设备管理 |
|
| |
| |
| |
| |
| |
|
早期的机架也很简单,机器的规模也不大
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport
解压镜像后,可在 Library/LaunchDeamons 目录下找到名为 com.apple.testmanagerd 的 plist 文件,打开后可以看到其 id 为 com.apple.testmanagerd(iOS14 后 id 有所变动)搞定 UI 交互后,接下来就需要找到能完美支持设备管理、应用管理、沙盒文件管理的方案。一开始我们也是想基于 idb 进行优化,但随后发现 idb 内部使用了大量的 api,这些 api 绝大部分缺少文档,优化成本较高,且这些私有 api 会随着 Xcode 版本的变动而更新,维护会很麻烦。所以放弃了 idb 转而寻找其他替代方案。
这时我们发现了另一个开源实现 libimobiledevice,libimobiledevice 支持通过 USB 的方式与 iOS 设备进行通信,且支持应用安装卸载、设备信息获取、沙盒文件操作等功能。libimobiledevice 在使用体验上,操作简单,功能稳定。但缺点是功能有限,不能完全符合我们的诉求,接下来我们对 libimobiledevice 的实现原理进行了探究。
苹果自身有一些 Mac App 需要通过 USB 跟 iOS 设备进行通信,比如 iTunes、XCode 及其套件等等。双端通信需要基于一定的协议,通过USB通信需要使用USB协议,但USB协议具有一定的局限性,直接使用成本较高。所以苹果在USB协议的基础上支持了TCP通信的能力,以此减小使用成本。
苹果通过 usbmuxd 来提供基于 USB 实现 TCP 通信的能力。usbmuxd是一个守护进程,它在 USB协议上实现了多路 TCP 连接,可以让应用层无感知的基于 USB 通道进行 TCP 通信。
/Library/Apple/System/Library/LaunchDaemons
,打开后如下:usbmuxd 的配置文件记录了加载属性、服务名称、可执行文件路径、socket 属性等信息。从上面的配置文件可以看到,usbmuxd 创建了一个 Unix 域的 socket。这个 socket 主要用于跟上层应用建立连接,实现跨进程通信。基于 usbmuxd 进行网络通信的流程如下:
/System/Library/LaunchDaemons/
目录。由此可以看出,lockdownd 分别支持 Unix 域的 socket 与非 Unix 域的 socket,对于非 Unix 域的socket,其监听的端口固定在62078。
当 PC 端想要跟 iOS 设备中的某个服务进行通信时,先通过 lockdownd 查找对应服务的端口,然后再跟对应的服务建立 socket 连接,其流程如下:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/
目录,当 iOS 设备连接 Xcode 时,Xcode 会自动将此目录下对应的DeveloperDiskImage挂载到 iOS 系统中。其包含的服务如下:/System/Library/LaunchDaemons
目录。通过越狱后,我们可以查看/System/Library/LaunchDaemons
目录下的内容,以下是部分服务的截图,可以看到,服务非常之多。搞清楚 libimobiledevice 实现原理后,基于这个思路,我们又自行探索了其他一些服务的能力,这其中就包括 Xcode 相关的服务。Xcode 为开发者提供了专业、稳定的工具集,而 Xcode 的工具集也是利用以上机制与 iOS 设备进行通信。基于Xcode 的能力,我们实现了 Trace 采集、设备应用管理等功能。
工具的问题解决了,接下来就是部署的问题。我们需要面对上千台 iOS 设备接入及每天上万次的服务调度,且设备之间的环境需要相互隔离,互不干扰。所以在设备及工具的部署上需要简单、高效。比较好的解决方案是采用 docker 部署,每台设备及其对应的驱动工具都用 docker 分离开,这样能做到环境隔离,且部署简单。但 Mac 对 docker 的支持并不那么友好,且我们的工具本身依赖 Xcode 环境。
既然 Mac 对 docker 的支持不友好,那我们是否能摆脱对 Mac 的依赖,将设备及工具部署在 Linux 上。顺着这个思路,我们开始了对工具的第二次改造。
工具对 Mac 的依赖主要来源于 XCTest 工具的启动,类似于 wda。我们将 XCTest 的接口封装在了一个普通的 App 里,然后在这个普通的 App 里搭建一个 websocket server,这样就可以通过网络与这个 App 通信,实现 XCTest API 的调用。但经过尝试后,发现普通的 App 调用 XCTest API 并不会产生预期的效果。所以还需要一些特殊的操作才能使普通App具备调用 XCTest API 的能力。
我们从正常的 XCTest-Runner 入手探索其启动流程。以home键的点击方法为切入点,通过 LLDB 追踪testmanagerd 进程中接口的调用流程,发现了如下关键的接口:
_IDE_authorizeTestSessionWithProcessID:
分析后发现,testmanagerd 会维护一个进程白名单,只有将 App 的进程 ID 加到这个白名单里,这个App 才具备调用 XCTest API 的能力。而上述注册白名单的接口则可以通过前面提到的 lockdownd 的方式调用。基于此,我们实现了摆脱对 Mac 环境的依赖,在 Linux 实现了 docker 化部署。
目前我们面向中小企业特别推出「APMPlus 应用性能监控企业助力行动」,为中小企业提供应用性能监控免费资源包。现在申请,有机会获得60天免费性能监控服务,最高可享6000万条事件量。