做者: imyzf本文将为你们介绍自动化控制 iOS 模拟器的原理,为开发基于 iOS 模拟器的前端调试方案提供帮助。javascript
咱们在开发 iOS App 内的前端页面时,有一个很大的痛点,页面没法使用 Safari Inspector 等工具调试。遇到了问题,咱们只能想办法加 vConsole,或者注入 Weinre,或者盲改,实在不行就找客户端同窗手动打包调试,总之排查问题的路途很是艰难。前端
在参考了 RN 和 Weex 等跨平台框架的开发工具后,咱们发现使用模拟器调试是解决该问题的很好方法,咱们将前端页面放到模拟器的 App 中运行,苹果就不会对其有限制,容许咱们使用 Safari Inspector 调试了。java
Safari Inspector 是和 Chrome Devtools 相似的调试工具,由 Safari 浏览器自带,支持如下功能:node
这些功能是 vConsole、Weinre 等工具没法比拟的,能够帮助咱们快速定位问题。ios
基于这些原理,咱们内部已经开发了一款工具,部分功能视频能够点此预览。但因为该工具和内部业务耦合较深,目前暂无开源计划。git
介绍这套方案以前,咱们须要了解一下方案的前提条件:github
咱们的模拟器调试方案总体流程如上图所示:npm
咱们在实现本方案时,主要基于如下工具:xcode
xcrun
对开发相关的功能进行控制,是一系列工具的集合。xcrun
提供了一个子命令simctl
用于控制模拟器,提供了模拟器的启动、关闭、安装应用、打开 URL 等功能。能够经过直接运行xcrun simctl
查看帮助文档。simctl
工具的 JS 封装。因为前端的方案通常都是基于 node.js 开发的,因此可使用 node-simctl 包更方便地控制模拟器。不过因为node-simctl
只提供了部分功能的封装,咱们依然须要手动调用xcrun
命令来实现更多功能。在本方案中,最重要的部分就是对模拟器的控制。浏览器
用户经过 App Store 安装完 Xcode 后,第一次运行须要赞成苹果的许可协议,而后自动安装一些组件,以后才能够正常使用。为了提升易用性,咱们但愿自动处理这个过程,而不是告诉用户,安装 Xcode 后要采起一些操做。
首先咱们能够尝试运行一次 xcrun simctl
命令,若是用户第一次运行,错误信息中会提醒用户手动运行xcodebuild -license
接受许可,因此咱们能够在错误信息中搜索xcodebuild -license
字符串,若是有找到,就自动动运行xcodebuild -license accept
命令,帮助用户自动接受许可。这里要注意的是,运行该命令须要 root 权限,可使用sudo-prompt
等包提权运行命令。
咱们能够直接使用 node-simctl 的getDevices()
函数获取本地安装的全部设备列表,比调用命令行更方便,能够直接获取到一个对象,不须要本身解析,对象部分结构以下:
{ '13.4': [ { sdk: '13.4', dataPath: '/Users/xx/Library/Developer/CoreSimulator/Devices/xxx/data', logPath: '/Users/xx/Library/Logs/xxx', udid: 'C1AA9736-XXX-YYY-ZZZ-2A4A674B6B21', isAvailable: true, deviceTypeIdentifier: 'com.apple.CoreSimulator.SimDeviceType.iPhone-11-Pro-Max', state: 'Shutdown', name: 'iPhone 11 Pro Max', platform: 'iOS' } ] ]
这里不只包含了 iPhone,还有 Apple Watch 和 Apple TV 等设备,咱们能够遍历返回结果,经过name
字段进行过滤,由于通常咱们只须要在 iPhone 中进行调试。
首先咱们要判断设备是否已经启动,咱们能够经过 xcrun simctl bootstatus ${deviceId}
命令获取设备状态(这里的 deviceId 即上面获取设备列表获得的udid
),可是若是设备没有启动,这个命令会一直等待,不会退出,因此咱们能够经过这个特征,基于命令是否超时(例如 1000ms 未返回结果)来判断设备是否启动。
接下来,就能够直接用xcrun instruments -w ${deviceId}
命令,启动对应的设备了。
代码示例:
let status = ''; try { status = execSync( `xcrun simctl bootstatus ${deviceId}`, { timeout: 1000 } ); } catch (error) { // 若是模拟器未启动,会一直等待,而后超时 kill,抛出一个 ETIMEDOUT 异常 if (error.code !== 'ETIMEDOUT') { throw error } } // 检查是否启动 if (status.indexOf('Device already booted') < 0) { console.log('正在启动模拟器……') execSync(`xcrun instruments -w ${deviceId}`) }
模拟器的安装包是一个以.app
为结尾命名的文件夹,和 macOS 应用相似,而不是 iPhone 真机上安装使用的.ipa
包。因此安装包须要先用zip
等工具进行打包上传到服务器,安装前下载到本地解压,使用 node-simctl 的installApp()
方法进行安装。
对于用户是否安装了 App,实际上是在经过分析唤起 App 的错误信息来判断的。若是 App 未安装,会在唤起的时候会报错,错误信息中包含了domain=NSOSStatusErrorDomain
字符串,表示 App 没有安装,这个时候咱们去调用上面的安装流程便可。
整个流程中最重要的一步是如何将咱们的页面在 App 中打开,实际上很简单,只须要 App 自己支持相似 cloudmusic://open?url=xxx
这样的 URL Scheme 便可。咱们经过 node-simctl 的openUrl()
方法直接调用 scheme,模拟器便会帮咱们启动关联的 App,而后须要 App 根据接收到的 Scheme 参数,帮咱们打开须要调试的页面。
代码示例:
try { await simctl.openUrl(deviceId, url) } catch (error) { // 没有安装 App,打开协议会报 NSOSStatusErrorDomain if (error.message.indexOf('domain=NSOSStatusErrorDomain') >= 0) { await simctl.installApp(deviceId, appPath) await simctl.openUrl(deviceId, url) } else { throw error } }
在模拟器中打开调试页面之后,对于 RN 页面,咱们能够用 React Native Debugger 等工具调试。对于 H5 页面,咱们能够从 Safari 菜单中打开 Inspector调试(若是没有“开发”菜单,请在 Safari 偏好设置 - 高级 - 选中在菜单栏中线显示“开发”菜单
)。
固然这一步也能够实现自动化,须要借助 Apple Script 搜索 Safari 菜单中的关键字并模拟点击,有点复杂,而且随着系统升级可能会失效,能够参考网上的一些讨论。
至此,咱们已经了解了如何控制模拟器,实现最基本的功能,可是咱们还能够对方案进行扩展实现,提升易用性。
客户端会按期发布新版本,加入新的功能,因此咱们也须要保持调试用的包为较新版本。通常客户端团队都会搭建本身的 CI 服务(例如 Jenkins)进行打包,因此咱们能够进行接入,自动下载和安装最新的包。甚至咱们能够拉取 CI 服务器上的包列表,实现安装历史版本,回归调试一些功能。
须要注意的是,客户端团队通常只针对 ARM 架构打包,因此须要在 CI 上新增 x86 构建目标,构建产物才能成功在模拟器上运行。
随着公司业务范围的拓展,咱们可能须要在多个 App 内调试页面,经过指定如下两点,能够实现多 App 的适配:
com.netease.cloudmusic
这样的字符串,是 App 的惟一标识,能够经过这个 ID 来进行 App 的启动、终止、卸载等操做到此为止,咱们介绍了构建一套基于 iOS 模拟器的前端调试方案的基本原理,基于以上内容,咱们能够结合 commander 和 inquirer 开发出一套 CLI 工具,也能够结合 Electron 开发一套 GUI 工具,为开发提效。若是你有更多的想法或者相关经验,也欢迎在评论区与咱们交流~
本文发布自 网易云音乐前端团队,文章未经受权禁止任何形式的转载。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们!