概述
iOS开发桥(idb)是一个多功能的工具,用于自动化iOS模拟器和设备。它在一个一致的、对人友好的界面中暴露了很多分布在苹果工具中的功能。
安装
idb由两个部分组成,每个部分都需要单独安装。
idb伴侣
每个目标(模拟器/设备)都会有一个附属进程,允许idb进行远程通信。
idb伴侣可以通过brew安装,也可以从源码构建
brew tap facebook/fb
brew install idb-companion
注意:关于如何安装brew的说明可以在这里找到
idb客户端
提供了一个cli工具和python客户端来与idb交互。
它可以通过pip安装:
pip3.6 install fb-idb
注意:idb客户端需要安装python 3.6或更高版本。
注意:关于如何安装pip的说明可以在这里找到
确保idb的两个部分都安装成功,然后你就可以开始了 请到快速入门处尝试一下idb。
快速入门
这是一个快速入门指南,向你展示idb能做什么的一瞥。如果你还没有安装idb,请参考安装。
让我们从找出你的Mac上有哪些模拟器/设备开始。这将打印出你的Mac上的所有模拟器和所有连接的设备。
idb list-targets
启动其中任何一个。
idb boot UDID
试试下面的任何一个命令,并确保你通过–UDID来运行它们与正确的模拟器。
idb launch com.apple.Maps
idb record
idb log
现在让我们试着运行测试。这将安装模拟器上提供的测试包。
idb xctest install Fixtures/Binaries/iOSUnitTestFixture.xctest
为了验证它是否被正确安装,只要运行
idb xctest list
通过发布这些命令来运行测试。
run logic
将只是运行逻辑测试run app
将运行app测试run ui
将运行 ui 测试
idb xctest run logic com.facebook.iOSUnitTestFixture
idb xctest run app com.facebook.iOSUnitTestFixture com.apple.Maps
架构
idb 是由两个具有不同职责的组件组成的。这两个组件都是idb运行命令的必要条件。
idb cli
这是一个Python3 cli,暴露了idb所提供的所有功能。因为它是用Python写的,所以不需要从连接iPhone或iOS模拟器的Mac上运行。
cli本身是idb_companion客户端的一个薄包装。所有与idb_companion的通信都是通过gRPC完成的。这可以是通过TCP或Unix域套接字。
如果你愿意,这个客户端库可以被导入到你自己的python3代码中,或者可以从任何其他类型的自动化中调用CLI。
idb_companion
idb_companion是一个在MacOS上运行的Objective-C++的gRPC服务器。它与用于自动化仿真器和设备的本地API对话。它连接FBSimulatorControl和FBDeviceControl框架,这两个框架是整个idb项目的一部分。
当idb_companion作为gRPC服务器时,它是为一个单一的iOS目标(设备或模拟器)而做的。
此外,idb_companion还有一些故意不在python CLI中使用的命令,这些操作与iOS设备管理或模拟器生命周期的操作有关。
连接
idb cli默认在两种模式中的一种运行:
- 如果idb cli运行在macOS上,那么它将自动启动和停止对所有连接到你的Mac的目标的编译。这意味着你可以对你拥有的任何iOS模拟器,以及你连接的任何设备运行命令。
- 如果idb cli运行在任何其他操作系统上,它将不会为你管理同伴。在这种情况下,你可以通过idb connect "附加 "同伴,或者在每次调用时使用IDB_COMPANION=hostname:port环境变量明确地附加同伴。这允许你对运行在其他主机上的同伴执行idb命令。这些用于发现同伴的设施在macOS上也能工作。
设备、模拟器和仿真器之间的区别
iOS设备和模拟器的行为方式大不相同,模拟器的行为方式也与仿真器(例如Android模拟器)大不相同:
- iOS模拟器和它们的子进程,在主机操作系统上显示为普通进程。
- iOS模拟器运行的可执行文件是macOS的原生文件。这与安卓的模拟器不同,后者可以在各种主机操作系统上运行,将始终运行原生的安卓可执行文件,并可能在ISA之间进行转换。
- 由于iOS模拟器是作为macOS的本地进程出现的,许多macOS级别的API与文件和进程的交互工作是一样的。这对实现模拟器的功能很有用。iOS模拟器使用与主机macOS相同的内核,这也在一定程度上解释了一些Xcode版本对macOS版本的要求增加(在较新的iOS版本中可能有新的内核功能,这意味着模拟器需要通过macOS升级来实现这一功能)。
- iOS模拟器使用的系统框架与macOS不完全相同。这些框架在Xcode中捆绑的 "模拟器运行时间 "中实现。该运行时间包含的框架与iOS设备上的框架大致相同,只是它们是为macOS主机架构编译的。
- 由于模拟器是原生运行的,它们具有与主机类似的性能特征。在某种意义上,模拟器应用程序的性能与在macOS上运行的macOS应用程序相似。这通常意味着模拟器的性能大大高于仿真器,即使仿真器可以访问管理程序并运行相同的ISA。
- iOS模拟器有一个 "根 "目录的概念,它可以被认为是模拟器的文件系统。在模拟器内运行的应用程序仍然可以访问根目录以外的文件(模拟器内没有chroot’ing),所以能够操作根目录以外的文件。应用程序也能够操作他们自己的 "应用程序沙盒 "之外的文件,而在iOS设备上却不是这样。
- 在iOS模拟器中,这种缺乏隔离的情况是一把双刃剑。它可以使某些自动化案例的实现更加方便,但要确保模拟器能够访问有限的系统资源并不容易。例如,仿真器通常允许对 "客人 "操作系统所能消耗的内核或内存数量设置上限,而iOS模拟器可以访问主机上任何应用程序可以访问的相同资源。这使得在同一主机上运行的多个iOS模拟器更难相互隔离。例如,一个消耗大量系统资源的iOS模拟器应用程序将耗尽在同一主机上运行的其他应用程序、进程或模拟器的这些资源。
- iOS设备对进程之间以及与之相连的主机之间的隔离是非常严格的。主机和附加的iOS设备之间的互动只有通过专门建立的API才能实现,这些API为现有的主机-设备行为提供了功能。例如,iOS应用的启动是通过 "仪器 "应用所使用的API在iOS设备上实现的。这种功能通常是通过套接字传输提供的,根据不同的领域,有不同的协议实现。对这些服务的访问是通过运行在iOS设备上的锁定服务来仲裁的。
- 因此,跨iOS模拟器和设备的功能实现是截然不同的,甚至在Xcode自己的框架中也是如此。
框架概念
在 FBSimulatorControl 和 FBDeviceControl 中有两个框架,用来实现 idb 使用的大部分功能。此外,还有一个FBControlCore框架,它的存在是为了定义设备和模拟器框架的通用接口,并提供两者通用的其他功能。这些框架能够独立于idb本身使用。以下是这些框架是如何被设计在一起的概述
目标
一个目标(FBiOSTarget)的实例是一个代表单个iOS模拟器或设备的对象。FBiOSTarget是一个协议定义,描述了由FBSimulator和FBDevice实现的功能。这种抽象意味着更高级别的应用程序和框架能够同样对待一个目标,无论它是一个iOS模拟器还是设备。
由于iOS模拟器和设备的操作方式有很大的不同,这种抽象级别允许框架在实现通用功能时平滑地处理这些差异。
目标集
一个 “目标集”(FBiOSTargetSet)表示一个目标的集合。这些在FBSimulatorSet和FBDeviceSet中都有实现。一个模拟器集代表一个根目录,该目录对许多模拟器来说是通用的。一个设备集代表所有连接到主机的设备。
这种抽象允许对模拟器和设备进行 "CRUD "操作的接口,尽管有不同的实现方式。例如,在模拟器和设备之间使用相同的API来擦除它们。
配置值
在整个框架中,有 "配置 "值,以Objective-C类实现。这些通常用于整合特定API调用所需的所有信息。例如,FBApplicationLaunchConfiguration定义了启动参数、环境和启动模式。
这些类型的存在是为了让API不需要极其冗长和繁琐的参数列表,以及提供合理的默认值。这些类型有意地尽可能地没有行为,接近于纯值类型。
命令协议和实现
为了在模拟器和设备之间保持一个共同的API,idb有一套Objective-C协议,为iOS模拟器和设备的单独实现定义了一个接口。这确实鼓励了精心设计的API的创建。idb_companion可以与底层的iOS目标无关,而是与这些协议进行交互。
可能有一些协议只被一个或另一个支持,这取决于目标。例如,在iOS模拟器上没有 "激活 "的概念,所以FBDeviceActivationCommands只由FBDevice实现。如果一个目标不支持给定的协议,当这些API被调用时,idb会失败,给出一个错误信息,解释缺少什么功能。
模拟器和设备之间的实现是完全分开的,并在它们各自的框架中实现。例如,FBApplicationCommands(它提供了一个在iOS目标上启动和列出应用程序的API),在FBSimulatorApplicationCommands和FBDeviceApplicationCommands中分别实现。
如果功能对模拟器和设备都是通用的,它的协议就会被添加到FBiOSTarget中,这样FBiOSTarget的实现者就需要实现它。对于不常见的功能,相关协议被添加到具体的FBSimulator或FBDevice类的定义中。对于非通用的协议,调用者必须在调用API之前检查协议的一致性,或者直接使用具体的类型。
记录
FBControlCoreLogger被用于整个代码库。它提供了一个通用的接口,用于向系统级记录器以及文件进行日志记录。由于所有这些框架可能被用于各种不同的场景,包括日志客户端可能是远程的,这个抽象提供了一个通用的方式,将日志引导到适当的地方。这是一个 “非结构化的记录器”,它接收任意的字符串。
还有一些类用于拦截内部调用(FBLoggingWrapper),并将其记录到 “结构化日志器”(FBEventReporter)。这在idb中被用来对服务器中的所有API调用产生准确的日志记录。这支持用户定义的类,所以非常适合推送到支持聚合的数据存储中。
IO
由于框架中所提供的功能的性质,IO是一项非常常见的任务。有许多抽象用于在各种源和汇之间读和写数据。例如,通用接口(FBFileReader)被用来从一个生成的应用程序进程中读取输出,并将其转发给消费者(FBFileWriter)。然后,这被用来通过idb的gRPC接口来管理应用程序的输出,而应用程序的启动器不需要知道是什么在消费输出。
所有这些都是由libdispatch支持的,由于它对异步IO的支持,文件的读写是以一种有效的方式管理的,而用户不必建立自己的IO复用器。
FBF未来
在框架内完成的大量 "工作 "都是基于IO和调用其他执行IO的API。这项工作是非常异步的,这意味着有充分的理由为执行和等待这项工作提供一个一致的API。
由于框架是Objective-C框架,以及Swift语言中没有像ync/await这样的API,FBFuture类被用来封装这种异步工作。这些功能最好存在于语言或标准库层面,以避免实现这类功能,不过这样做也有好处:
- 错误条件。几乎所有由FBFuture代表的异步操作在某种程度上都是易错的,因此FBFuture可以用一个完整的NSError来解决错误状态。
- 链式。一个单一的高层API可以由一个接一个的异步调用序列构成。FBFuture语法提供了一种将这些东西以一种准高效的方式串联起来的方法。
- 队列必须始终被定义。为了防止非预期的行为,即一个异步回调被调用到一个任意的或私有的队列中,FBFuture的任何消费者必须提供回调将被调用的队列。这也是链的真实情况,它促进了用于序列化调用其他API的队列或用于后台行为的队列的分离,这些队列可以在任何线程上执行。例如,对CoreSimulator的所有调用必须在同一个线程上进行序列化,但在Future中执行没有副作用的纯数据转换的工作可以在一个任意的后台队列中执行。
- 没有等待者线程。所有Future的解析都是异步进行的,不需要有一个线程或队列在等待Future的解析。这意味着,如果多个Futures同时运行,就不会有线程耗尽的危险。
作为FBFuture的消费者的代码可以使用它的调用来接收完成后的回调,或者等待:它的结果同步地获得一个值。FBFuture的内部使用尽可能地避免了任何同步工作。idb的消费者不需要知道这些细节,因为它们是idb_companion的内部实现。
idb_companion的概念
idb_companion的大部分工作是作为一个gRPC服务器来实现FBSimulatorControl和FBDeviceControl框架的所有功能。
它确实有一些组件对它的运行方式很重要。
main
这是idb_companion的入口,包括所有它支持的各种标志。它用于指定如何为一个给定的iOS目标启动gRPC服务器。
此外,它还暴露了一些 "CRUD "命令,这些命令对于管理模拟器和设备是有破坏性的。这些命令是故意不在gRPC接口中出现的,以防止不必要的行为。如果你想执行破坏性的命令,这些必须在iOS模拟器或设备所在的主机上执行。
FBIDBServiceHandler
这是一个C++类,实现了gRPC接口。这是一个C++类,因为idb_companion正在使用C++的gRPC库。
请求被转发到gRPC C++库内部的线程池上的处理程序。绝大多数对同伴的调用都是调用由FBFuture支持的API。这意味着处理程序线程将负责等待该FBFuture的解析。每个处理程序的调用都被包裹在一个自动释放池中,以防止内存泄漏。对底层框架的调用将适当地序列化工作,因为它们使用了Future。
FBIDBCmandExecutor
这是一个纯粹的Objective-C类,它为底层框架中的许多API提供了一个门面,所以服务处理程序不需要知道它们是如何操作的。这也意味着C++只需要在必要的地方使用,所以如果gRPC处理程序被迁移到Swift,处理程序中的实现将是最小的。
在调用命令执行器之前,服务处理程序需要将C++类型转换为与之对应的Objective-C类型,因为请求和响应对象是通过C++ protobufs表达的。
开发
构建和运行idb cli
idb cli是基于python的,可以简单地用pip来构建
pip3 install .
构建和运行idb_companion
这是一个本地的macOS可执行文件,通过Xcode构建。
首先,需要有系统级的构建依赖,这些可以通过homebrew安装:
# Tap for grpc
brew tap grpc/grpc
# The grpc compiler is used to generate C++ bindings from the idb.proto definition
brew install grpc
# cocoapods is needed to resolve dependencies for the Xcode project
brew install cocoapods
# cocoapods is used to resolve the grpc runtime library for the companion
# This must be run from the root of the idb repository to use the appropriate Podfile
pod install
这将打开一个Xcode项目,你可以构建和运行:
open idb_companion.xcworkspace
打开Xcode项目后,你需要添加一个–UDID参数来启动。
- 获取你的设备或模拟器的UDID
- 窗口 -> 设备和模拟器
- 选择你关心的设备或模拟器
- 复制头文件中的标识符部分的值
- 项目 -> 方案 -> 编辑方案(或 cmd + <)。
- 运行 -> 参数
- 点击启动时传递的参数部分下的 “+”。
- 输入 --udid <上面复制的UDID>。
- 在我的Mac上运行idb_companion目标
一旦idb_companion启动,它将把同伴所绑定的TCP端口输出到stdout:
{"grpc_port":10882}。
默认情况下,这个端口是10882,它可以用–端口0或你选择的端口来绑定一个随机端口。现在你可以通过传递给cli的IDB_COMPANION环境变量来指挥针对这个同伴的IDB命令:
$ IDB_COMPANION=localhost:10882 idb describe
只要你在所有的命令前加上这个环境变量的前缀,你就可以在Xcode中对你目前正在调试的同伴运行命令。
命令
iOS目标操作
所有idb cli命令都是针对一个特定的iOS目标(模拟器或设备)运行的。由于一个主机几乎肯定会有一个以上的目标连接,所以需要有一种方法来指定哪个iOS目标来运行命令。
所有iOS目标的共同点是存在一个UDID。iOS模拟器的UDID是基于NSUUID的,设备的UDID则取决于手机型号,但iPhone Wiki有一个格式概述。就idb而言,UDID只是一个字符串。
idb cli由idb_companion驱动,以执行所有的底层行为。由于idb_companion可以通过域套接字或TCP套接字暴露出来,所以也可以直接寻址一个同伴,而不是通过UDID寻址。
对一个特定的iOS目标运行命令
idb维护它所知道的iOS Target的本地状态。这个状态可以被修改或绕过。idb在很大程度上依靠idb_companion来处理请求。因此,所有操作iOS Targets的命令都需要一个同伴来服务它们。
如果你能在每个调用中传递同伴的位置,那么就不需要修改这个内部状态。所有的idb命令都可以以IDB_COMPANION环境变量为前缀,这将直接解决一个给定的同伴。如果提前知道同伴的位置(而不是只有一个UDID),这是寻址iOS目标的首选方式:
# Run a describe command against a companion running on the loopback interface
# on TCP Port 10882
$ IDB_COMPANION=localhost:10882 idb describe
# This can also be a path to a domain socket that the companion is running on
$ IDB_COMPANION=/tmp/idb_companion_domain_sock idb describe
通过UDID而不是同伴地址来寻址是通过连接同伴来实现的,因此这种知识在调用中会持续存在:
# Connecting via a TCP companion server
idb connect COMPANION_HOST COMPANION_PORT
SOME_UDID
# Connecting via a unix domain socket
idb connect COMPANION_DOMAIN_SOCKET_PATH
SOME_UDID
# SOME_UDID can then be used to address the connected target via --udid
idb还可以在后台透明地启动同伴,因此用户不需要知道如何启动同伴或直接寻址:
# This will implictly start a companion in the background and keep it alive
$ idb connect TARGET_UDID
TARGET_UDID
使用connect也是可选的;当针对给定的UDID执行命令时,如果没有为给定的UDID运行同伴,将以类似于idb connect的方式在后台启动一个同伴:
# When TARGET_UDID does not have a companion running for it, running describe will run it in the background.
$ idb describe --udid TARGET_UDID
断开一个目标的连接
idb disconnect COMPANION_HOST COMPANION_PORT
idb disconnect TARGET_UDID
与connect相反。这不会终止支持这个目标的同伴。
列出连接的目标
idb知道你手动连接的同伴,以及其他还没有同伴的iOS目标。这可以用list-targets来显示:
idb list-targets
输出将显示哪些目标有或没有同伴与之关联。
启动一个模拟器
idb --boot UDID
这只适用于 iOS 模拟器。
描述一个目标
idb describe
返回关于指定目标的元数据,包括:
- UDID
- 名称
- 屏幕尺寸和密度
- 状态(已启动/…)。
- 类型(模拟器/设备)
- iOS版本
- 架构
- 关于其同伴的信息
一般参数
除了与特定命令相关的参数外,还有一些适用于所有命令的可选参数。
参数 | 描述 | 默认 |
---|---|---|
–udid UDID | 目标的UDID | 如果只有一个目标被连接,它将使用那个目标。 |
–log {DEBUG,INFO,WARNING,ERROR,CRITICAL} | 设置日志级别 | CRITICAL |
–json | JSON结构化输出(如适用) | False |