tidevice 助你在非Mac环境执行iOS自动化

前言

一直以来,iOS自动化的实现&执行都依赖 Mac 系统,其主要原因是因为需要通过 xcodebuild 编译&安装 WDA (WebDriverAgent) 到 iOS 设备中,通过WDA实现对被测应用进行操作。

导致想要做iOS自动化 就必须拥有 Mac 设备的现象

  • 常用电脑非 Mac 的同学 想要做 iOS 自动化的时候, 就需要再申请一台Mac设备 ,可能会出现资源利用不充分的情况

  • 云测试平台要搭建 IOS 自动化 服务环境时,也只能批量申请 Mac 设备,经费在燃烧

一个月前,阿里团队开源了一个内部使用的 iOS自动化工具 :tidevice (https://github.com/alibaba/taobao-iphone-device) ,让我们可以更方便、简单的脱离Mac的限制。

本文会和大家分享下 tidevice 调研记录 以及 如何集成到现有方案中


tidevice 能做什么

  • 设备信息获取

  • 应用安装、卸载、启动、停止、查看应用信息、已安装应用列表

  • 启动 WebDriverAgent (不依赖 xcodebuild , 跨平台)

  • 运行 XcTest UITest

  • 性能数据采集

  • 设备截图、设备日志 ...


tidevice 核心原理

usbmux通信协议:

实现 Mac/Windows/Linux 与 iOS设备服务间的通信

  • Mac

    • usbmuxd 是苹果的一个服务,这个服务主要用于在USB协议上实现多路TCP连接,将USB通信抽象为TCP通信。苹果的iTunes、Xcode,都直接或间接地用到了这个服务。

  • Linux / Windows

    • 本身是没有 usbmux的,不过都有开源项目的实现,可以直接使用/参考

    • Windows 另外依赖 AppleApplicationSupport和AppleMobileDeviceSupport 两个服务,安装Itunes 环境即可安装对应服务

usbmux 本身是socket 套接字,通过截获、破解等手段,结合开源界的成果,用python 进行模拟,从而实现了当前工具已有的所有功能


tidevice 环境安装

必须已有 Python 3.6+ 环境

  • tidevice 安装

    • pip3 install -U "tidevice[openssl]” (推荐)

    • pip3 install -U tidevice (缺少设备配对功能)

  • usbmuxd

    • Mac 自带:/var/run/usbmux

    • Linux/Windows: 参考官方建议的 https://github.com/alibaba/taobao-iphone-device/issues/7


tidevice 现支持cmd 及 Python 模拟实现

因为转转客户端UI自动化框架 是Python工程,所以当发现tidevice 也是Python工程后,对tidevice功能的具体实现很感兴趣,希望可以直接调用其内部实现进行集成,而不是封装 cmd ,在调研的同时,根据转转侧需要,进行了部分功能的重新封装。

tidevice 支持的所有cmd 都在 tidevice.__main__ 中定义实现

以下 cmd 的Python实现,会直接参考/复用 main 中实现 ,大家也可以根据自己的封装习惯,重新封装

设备管理

查看已连接设备列表
  • cmd:

tidevice list
  • Python:

from tidevice import Usbmux
print(Usbmux().device_list())
监听设备连接状态

当有设备连接、断开连接时,都会实时返回内容

  • cmd:

tidevice watch
  • Python:

from tidevice import Usbmux
for info in Usbmux().watch_device():
    print(info)

Usbmux().watch_device() 返回 generator , 利用for 循环调用 ,但这里会阻塞后续逻辑,所以最好用Process 单独监听处理

等待任意设备连接,连接一台就结束等待
  • cmd:

tidevice wait-for-device
  • Python:

from tidevice import Usbmux
for info in Usbmux().watch_device():
   if info["MessageType"] == "Attached":
        break
等待指定设备连接
  • cmd:

tidevice -u $UDID wait-for-device 
  • Python:

from tidevice import Usbmux
for info in Usbmux().watch_device():
   if info["MessageType"] != "Attached":
        continue
   udid = info["Properties"]["SerialNumber"]
   if udid == "$UDID":
        break
打印设备日志
  • cmd:

tidevice -u $UDID syslog
  • Python:

from tidevice import Device
d = Device("udid")
d.start_service("com.apple.syslog_relay")
while True:
   print(s.recv().decode("utf-8"))

应用管理

安装应用
  • cmd:

tidevice --udid $UDID install example.ipa
  • Python:

from tidevice import Device
Device("udid").app_install(ipa_url_or_path)
卸载应用
  • cmd:

tidevice --udid $UDID uninstall com.example.demo
  • Python:

from tidevice import Device
Device("udid").app_uninstall("com.example.demo")
启动应用
  • cmd:

tidevice --udid $UDID launch com.example.demo
  • Python:

from tidevice import Device
# 如果是已启动&最上层应用,则不变;如果已启动&退到后台,拉起,如果未启动,启动
pid = Device("udid").app_start("com.example.demo")
# 强制重新启动
pid = Device("udid").app_start("com.example.demo", kill_running=True) 
停止应用
  • cmd:

tidevice --udid &UDID kill com.example.demo 
  • Python:

from tidevice import Device
# 如果传 app_start生成的 pid ,可以直接杀掉,如果传 com.example.demo 也可以杀掉
Device("udid").app_stop(pid_or_name) 
查看已安装应用
  • cmd:

tidevice —udid &UDID applist
  • Python:

from tidevice import Device
Instruments = Device("udid").connect_instruments()
# 设备上全部App信息列表 包含 系统应用和插件,通过 Type 可以区分App
apps = instruments.app_list() 
# 只筛选用户安装的App列表
user_app_list = [app for app in apps if app["Type"] == "User"] 
查看应用信息

名称、版本、权限、依赖、插件 等包信息

  • cmd:

tidevice appinfo com.example.demo
  • Python:

from tidevice import Device
Device("udid").installation.lookup("com.example.demo")
通过ipa下载链接分析ipa信息
  • cmd:

 tidevice parse ipa_url
  • Python:

import httpio
from tidevice._ipautil import IPAReader
fp = httpio.open(ork, block_size=1)
ir = IPAReader(fp)
ir.get_infoplist()
通过ipa文件分析ipa信息
  • cmd:

tidevice parse ipa_path
  • Python:

from tidevice._ipautil import IPAReader
fp = open(ipa_path, "rb")
ir = IPAReader(fp)
ir.get_infoplist()

执行自动化

执行XCTest (需要先确保手机上已经安装有WebDriverAgent,并且知道bundleId)
  • cmd:

tidevice xctest -B com.facebook.wda.WebDriverAgent.Runner
  • Python:

from tidevice import Device
Device("udid").xctest("com.facebook.wda.WebDriverAgent.Runner")

无论是在代码中通过cmd 执行,还是通过python库执行xctest,都会阻塞后续操作,所以建议使用 Process

执行XCTest 修改监听端口为8200,并显示调试日志
  • cmd:

idevice XCTestCase -B com,facebook.wda.WebDriverAgent.Runner -e USB_PORT:8200 —debug
  • Python:

from tidevice import Device
Import logging
logger = logging.getLogger("tidevice.xctest")
Device("udid").xctest("com.facebook.wda.WebDriverAgent.Runner", log=logger, evn={"USB_PORT": 8200})
Relay 转发请求到手机,类似于iproxy
  • cmd:

tidevice relay 8100 8100
  • Python:

from tidevice import Device
from tidevice._relay import relay
d = Device("udid")
relay(d, 8100, 8100)

同执行xctest ,无论是在代码中通过cmd执行,还是通过python库执行relay, 都会阻塞后续操作,所以建议使用Process

Relay 转发请求并把传输的内容用hexdump的方法打印出来
  • cmd:

tidevice relay -x 8100 8100
  • Python:

from tidevice import Device
from tidevice._relay import relay
d = Device("udid")
relay(d, 8100, 8100, debug=True)
运行XCTest 并在PC上监听8200端口转发到手机8100服务
  • cmd:

tidevice wdaproxy -B com.facebook.wda.WebDriverAgent.Runner —port 8200
  • Python:

    • 方式1:tidevice.__main__ 中cmd_wdaproxy方式

from tidevice._wdaproxy import WDAService
from tidevice import Device
d = Device("udid")
serv = WDAService(d, "com.facebook.wda.WebDriverAgent.Runner")
cmd = [sys,executable, "-m" , "tidevice" , "-u" d.udid, "relay", "8200", "8100"]
p = subprocess.Popen(cmd, stdout=sys.stfout, stderr=sys.stderr)
serv.start()
while serv._service.running:
    time.sleep(1)
p and p.terminate()
serv.stop()
  • 方式2:通过 xctest relay 结合

from tidevice import Device
from tidevice._relay import relay
from multiprocessing import Process
d = Device("udid")
p1 = Process(target=device.xctest, args=("com.facebook.wda.WebDriverAgent.Runner", None, None, {"USB_PORT":8200}))
p2 = Process(target=relay, args=(d, 8200, 8100, False))
p1.start()
p2.start()
..
p1.teminate()
p2.terminate()

运行后,可以通过 http://127.0.0.1:8200/status判断

运行XCTest UITest
  • cmd:

tidevice xctest —bundle-id philhuang.testXCTestUITests.xcrunner —target-bundle-id philhuang.testXCTest
  • Python:

from tidevice import Device
d = Device("udid")
d.xctest("philhuang.testXCTestUITests.xcrunner", target_bundle_id="philhuang.testXCTest")

获取设备信息

基础信息

型号、设备名、系统版本、手机号、序列号、时区、蓝牙地址、Wifi地址等

  • cmd:

tidevice info
  • Python:

from tidevice import Device
# 内容会比cmd 全很多, 但是需要理解每个字段的含义
Device("udid").device_info()
查看设备电源信息
  • cmd:

tidevice info --domain com.apple.mobile.battery --json
  • Python:

from tidevice import Device
import json
domain_info = Device("udid").device_info("com.apple.mobile.battery")
print(json.dumps(domain_info))

主要通过 --domain 参数 查看指定domain的信息

装有libimobiledevice 的电脑,可以执行 ideviceinfo -h 查看都有哪些domain

具体的domain信息 ,可以根据需求进行封装

系统信息
  • cmd:

tidevice sysinfo
  • Python:

from tidevice import Device
Device("udid").instruments.system_info()
电池信息
  • cmd:

tidevice battery
  • Python:

from tidevice import Device
Device("udid").get_io_power()

这里比 Device("udid").device_info("com.apple.mobile.battery") 获取的信息更全面,后者只是基础的当前电量和当前充电状态信息

fps 数据采集
  • cmd:

tidevice dumpsfps
  • Python:

from tidevice import Device
d = Device("udid")
for data in d.instruments.iter_opengl_data():
    if is instance(data, str):
        continue
    print(data["CoreAnimationFranesPerSecond"])

在 Device().instruments 中已经封装好了部分性能数据采集方法,可以二次封装使用

设备操作

截屏保存在当前目录
  • cmd:

tidevice screenshot
  • Python:

from tidevice import Device
import time
filename = "screenshot_" + str(time.time()) + ".jpg"
Device("udid").screenshot().conver("RGB").save(filename)
截屏保存在指定目录
  • cmd:

tidevice screenshot /xxxx/xxx/screenshot.jpg
  • Python:

from tidevice import Device
file_path = "/xxxx/xxx/screenshot.jpg"
Device("udid").screenshot().conver("RGB").save(file_path)
关机
  • cmd:

tidevice shutdown
  • Python:

from tidevice import Device
Device("udid").shutdown()
重启设备
  • cmd:

tidevice reboot
  • Python:

from tidevice import Device
Device("udid").reboot()
设备文件管理
  • cmd:

tidevice -u $UDID fsync
  • Python:

from tidevice import Device
Device("udid").sync
应用文件管理
  • cmd:

tidevice -u $UDID fsync -B com.example.app
  • Python:

from tidevice import Device
Device("udid").app_sync("com.example.app")
配对设备

如果已信任的设备不建议使用, 会重新信任,如果是远程设备,则不能自动操作信任窗口

  • cmd:

 tidevice -u $UDID pair
  • Python:

from tidevice import Device
Device("udid").pair()

tidevice 集成

转转方案当前底层使用的是Appium框架,同类方案可以参考

集成思路

  • 首先要提前将 WDA 安装到 iOS设备中 并在设置中信任开发者,确保WDA可以正常启动

  • 将 WDA 的bundle_id (Bundle Identifier) 作为 一个配置/常量,保存在 框架工程中, tidevice wdaproxy 需要

  • 在 通过 webdriver 启动driver 前,通过 tidevice 的 cmd 或者 自己封装实现的 wdaproxy 启动 WDA & 端口转发 ,这时候 local_port(本地端口)需要 记录

  • 在 通过 webdriver 启动driver 前,修改 driver 启动需要的配置

    • 添加 webDriverAgentUrl : "http://127.0.0.1:" + str(local_port)

    • 如果有设置 useNewWDA 为 True 的话,需要 改为 False

    • wdaLocalPort 也可以不设置了

  • 使用新的配置 启动 driver ,就可以执行测试逻辑了

  • 所有任务执行完成后,最好主动检查&回收 tidevice 进程

建议

  • 如果Mac环境 建议先保留原有的流程

    • 在集成后实际测试使用过程中发现,tidevice wdaproxy 方式,wda不是很稳定,偶尔会出现通信异常,重启wda的现象,具体原因还没有分析出来,如果后续多次验证其稳定后,可以再根据实际需求决定是否全都使用tidevice wdaproxy

  • 如果Mac环境保留原有的流程 , 别忘记增加 是Linux/Windows 还是 Mac 环境的判断


总结

tidevice工具,使iOS自动化摆脱了Mac的限制,给iOS自动化方案建设更多的可能

  • 可以在Windows 环境 通过 tidevice wdaproxy 启动WDA, 使用Appium Client 分析页面 & 编写Case

  • 可以在Linux 服务器上 通过 tidevice 搭建 iOS设备自动化 远程执行环境

  • 可以在Mac/Windows/Linux 通过 tidevice 采集性能数据,集成到自动化测试流程中

在看了项目源码后,发现 tidevice 提供的 cmd 只使用到部分底层代码实现,可以根据自己的需求,立刻封装出更多功能,也可以参考源码通过 usbmux 实现更多功能

最后,感谢阿里团队开源tidevice工具,希望本篇可以帮助到大家

还在等什么,快去试用、集成、看源码吖~

如果喜欢这篇分享的话,辛苦关注、转发、在看、点赞、评论、赞赏吖~

end


原文地址:https://www.cnblogs.com/finer/p/14811683.html