作者:字节跳动终端技术——陈友辉

一、背景

随着业务的扩张,单个 App 的功能越来越多,工程复杂度越来越高,每天MR可达上百次,代码变更可达上千处,航母级的 App 在这一点上更为严重。如何在频繁的代码变更中保障App质量,成了各个业务的痛点。靠传统的人工测试已无法满足各业务的需求,我们需要将更多的测试场景自动化。

自动化测试需要将人工交互行为变成自动化的原子操作。比如应用安装卸载、屏幕点拖拽及缩放、实体按键点击、设备信息获取、应用启停等等。这就需要一款工具来驱动 iOS 设备完成以上操作。这篇文章主要介绍字节 iOS 自动化测试驱动工具 bdc 的探索过程及实现原理。

二、功能介绍

在介绍 bdc 的探索过程及实现原理之前,先介绍一下 bdc 的能力:

三、探索历程

早期方案

在字节开始大规模建设自动化建设时,Android 已经有较为完善的解决方案,包括其生态自带的驱动工具 adb 及开源的云真机管理平台 STF。但 iOS 在这方面相对滞后,主要是 iOS 缺少一款类似 adb 功能齐全且稳定的驱动工具。

早期 iOS 采用了 Facebook 开源的方案,Facebook 在驱动工具方面先后开源了 wda 与 idb,wda 支持 UI 交互操作,idb 支持应用管理,这在一定程度上满足了我们的需求,基于这套方案,搭建了第一个版本的自动化测试机架。

早期的机架也很简单,机器的规模也不大 经过一段时间的实践,我们遇到了以下几个问题

  • wda 部分接口执行耗时较长,效率低下,无法满足高频率调度的需求
  • idb 很多命令只支持模拟器,对真机不够友好,无法满足我们的功能扩展
  • 命令执行失败率高,工具稳定性差,且出问题后难以排查
  • 整套流程强依赖 Xcode 环境,规模化、自动化部署成本高,无法应对上千台手机的部署

工具改良

UI 交互改造

为了解决上述问题,我们结合 wda 的实现思路,实现了一个更高效稳定的 XCTest 工具。我们对 XCTest 相关的接口进行了review,并找到了XCTest 实现跨进程调用最底层的接口。通过这些接口,可以直接调用 testmanagerd 进程。随后基于这些底层接口封装了一套新的接口,可以实现屏幕的点拖拽、实体按键点击、文本输入等操作。相比 wda,在执行速度和稳定性上获得大幅提升。

testmanagerd 进程是一个开发者守护进程,在 iOS 设备开启开发者模式后,testmanagerd 进程的镜像会被挂载到 iOS 设备系统的 Developer 目录,并被 launchd 进程启动。XCTest 使用苹果自带的 XPC 机制与 testmanagerd 进行通信,利用 NSXPCConnection,只需知道服务的 id 即可建立通信,testmanagerd 服务的 id 可在其镜像文件里找到。镜像的路径位于:/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 的实现原理进行了探究。

基于 USB 与 iOS 设备通信

苹果自身有一些 Mac App 需要通过 USB 跟 iOS 设备进行通信,比如 iTunes、XCode 及其套件等等。双端通信需要基于一定的协议,通过USB通信需要使用USB协议,但USB协议具有一定的局限性,直接使用成本较高。所以苹果在USB协议的基础上支持了TCP通信的能力,以此减小使用成本。

苹果通过 usbmuxd 来提供基于 USB 实现 TCP 通信的能力。usbmuxd是一个守护进程,它在 USB协议上实现了多路 TCP 连接,可以让应用层无感知的基于 USB 通道进行 TCP 通信。

macOS 上的 usbmuxd 配置文件位于/Library/Apple/System/Library/LaunchDaemons,打开后如下:

usbmuxd 的配置文件记录了加载属性、服务名称、可执行文件路径、socket 属性等信息。从上面的配置文件可以看到,usbmuxd 创建了一个 Unix 域的 socket。这个 socket 主要用于跟上层应用建立连接,实现跨进程通信。基于 usbmuxd 进行网络通信的流程如下:

发现iOS设备系统服务并完成调用

从上述 usbmuxd 的通信流程可知,想要跟iOS设备中的服务进行通信,只需要知道具体服务的端口即可。那么服务端口该如何获取?一般有两种方式,一种是直接hardcode,另一种是动态获取。iOS设备内部的服务众多,如果全部 hardcode,其维护成本较高,且安全性及稳定性较低。如果是动态获取,那么获取的方式将成为另一个问题,因为动态获取本身也需要通信。

苹果则采用了两者结合的方式。苹果在iOS系统内部增加了一个 lockdownd 的守护进程,这个守护进程以 root 特权运行,具有访问 iOS 系统信息的能力,且运行在固定的端口。lockdownd 的配置文件如下,其路径位于 iOS 系统的/System/Library/LaunchDaemons/目录。

由此可以看出,lockdownd 分别支持 Unix 域的 socket 与非 Unix 域的 socket,对于非 Unix 域的socket,其监听的端口固定在62078

当 PC 端想要跟 iOS 设备中的某个服务进行通信时,先通过 lockdownd 查找对应服务的端口,然后再跟对应的服务建立 socket 连接,其流程如下:

libimobiledevice 就是利用以上流程对 iOS 设备中的服务进行了调用,比如在操作沙盒文件时,就是调用了 iOS 设备中的 afc、house_arrest 服务。

服务类型

搞清楚了调用流程,那么 iOS 设备中都有哪些服务呢?iOS 系统服务主要分为两类:一类是debug相关服务,打开开发者选项后才具备相应服务。另一类是非 debug 服务,默认都具备。

debug 相关服务的配置文件及可执行文件都被打包放在了 Xcode 中,位于/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/目录,当 iOS 设备连接 Xcode 时,Xcode 会自动将此目录下对应的DeveloperDiskImage挂载到 iOS 系统中。其包含的服务如下: 上述提到的 testmanagerd 就位于其中。

非debug服务位于iOS系统的/System/Library/LaunchDaemons目录。通过越狱后,我们可以查看/System/Library/LaunchDaemons目录下的内容,以下是部分服务的截图,可以看到,服务非常之多。

能力扩展

搞清楚 libimobiledevice 实现原理后,基于这个思路,我们又自行探索了其他一些服务的能力,这其中就包括 Xcode 相关的服务。Xcode 为开发者提供了专业、稳定的工具集,而 Xcode 的工具集也是利用以上机制与 iOS 设备进行通信。基于Xcode 的能力,我们实现了 Trace 采集、设备应用管理等功能。

支持 Linux

工具的问题解决了,接下来就是部署的问题。我们需要面对上千台 iOS 设备接入及每天上万次的服务调度,且设备之间的环境需要相互隔离,互不干扰。所以在设备及工具的部署上需要简单、高效。比较好的解决方案是采用 docker 部署,每台设备及其对应的驱动工具都用 docker 分离开,这样能做到环境隔离,且部署简单。但 Mac 对 docker 的支持并不那么友好,且我们的工具本身依赖 Xcode 环境。

既然 Mac 对 docker 的支持不友好,那我们是否能摆脱对 Mac 的依赖,将设备及工具部署在 Linux 上。顺着这个思路,我们开始了对工具的第二次改造。

支持 XCTest 启动

工具对 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 化部署。

四、运行效果

bdc 工具上线已有一年多。目前支撑了公司自动化测试平台上千台 iOS 设备每日上万次的设备调度及测试任务执行。自动化测试平台涵盖了稳定性、UI、性能、单元测试等多项测试能力,服务公司上百个业务。

设备机架从原有的简陋设备已升级为全球机房,服务于全球业务。


🔥 火山引擎 APMPlus 应用性能监控是火山引擎应用开发套件 MARS 下的性能监控产品。我们通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。目前我们面向中小企业特别推出「APMPlus 应用性能监控企业助力行动」,为中小企业提供应用性能监控免费资源包。现在申请,有机会获得60天免费性能监控服务,最高可享6000万条事件量。

👉 点击这里,立即申请

03-06 00:05