做者: 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 工具,爲開發提效。若是你有更多的想法或者相關經驗,也歡迎在評論區與咱們交流~
本文發佈自 網易雲音樂前端團隊,文章未經受權禁止任何形式的轉載。咱們一直在招人,若是你剛好準備換工做,又剛好喜歡雲音樂,那就 加入咱們!