从Intel Mac切到M1芯片那阵子我最担心的反而不是性能而是手头那套UI自动化环境还能不能跑起来。说实话刚开始确实折腾得不轻Appium连不上模拟器、Selenium驱动一个session都建不出来、Homebrew装出来的工具隔三差五报架构错误。这些问题说到底都是Apple Silicon和Intel那套东西在指令集层面“语言不通”导致的。这篇文章把我从零搭建M1 Mac UI自动化环境的完整过程、踩坑记录和最终可行的方案都整理出来覆盖移动端Appium链路和Web端Selenium链路给正在迁移或准备入手M1做自动化测试的同学一个可以直接照做的参考。1. 为什么M1会逼疯一批做UI自动化的人架构差异先讲清楚1.1 从Intel到Apple Silicon环境配置的底层逻辑变了在Intel芯片时代Mac的CPU指令集是x86_64这套指令集和Windows、Linux桌面端的x86_64是同一套体系。你下载的工具、驱动、模拟器镜像只要编译成x86_64架构装上去基本都能跑。M1完全不同它用的是arm64架构就像一个只会中文的人突然到了只说西班牙语的国家两边对着讲话谁也不知道对方在说什么。苹果为了解决这个问题给出了Rosetta 2翻译层它能把x86_64的二进制“实时翻译”成arm64指令再执行。这个翻译层解决了很多日常软件的兼容问题比如你在M1上跑一些Intel版的老工具绝大多数情况都能正常打开。但UI自动化不一样它涉及的东西太多了包管理器、JDK、Node、Android SDK、iOS模拟器、浏览器驱动这些工具之间是有调用链关系的。翻译层只能保证单个程序能启动没法保证整条链路的底层接口全部对得上。一旦某个环节用了Intel版的二进制下一环调用它的时候就有可能出现诡异报错。实际体验下来很多“M1不兼容”的传言并不是软件本身不能跑而是安装的时候选错了架构版本。理解了这个底层逻辑后面很多配置步骤就不需要死记你知道自己在干什么。1.2 M1上真实的兼容性分层按这个思路判断工具选型我在搭建环境的时候习惯把工具分成三层来判断这一套思路在排查问题时非常管用层级状态典型工具风险说明原生arm64直接安装Chrome、VS Code、Node 18、OpenJDK 17、Appium 2.x只要版本下对基本不会出问题通过Rosetta 2运行能用但有隐患部分旧版命令行工具、Intel版JDK、旧版Android Gradle插件日常用没问题一旦涉及驱动调用、原生编译、模拟器交互就容易翻车几乎不可用别浪费时间HAXM、x86_64-only的驱动、老版本iOS模拟器Runtime底层架构不支持部分工具官方也不再维护判断一个工具到底是什么架构最快的办法就是在终端里用file命令看二进制文件file $(which java) # 如果输出包含 arm64说明是原生架构 # 如果输出包含 x86_64说明是Intel版这个命令我在整个搭建过程中反复使用。原因很简单你已经装了某个软件与其去官网重新查下载哪种安装包不如直接看本机二进制文件的架构这最接近真实情况。后面所有“版本不对”“驱动崩溃”的问题基本都是通过这一步定位到架构层面的。2. 动手之前的基础设施终端、包管理器与JDK的“双轨制”2.1 Homebrew的arm64版和x86_64版可以共存关键看你怎么管大多数人在M1上安装Homebrew用的还是官网那行安装命令正常情况下它会自动安装到/opt/homebrew目录这是arm64原生版。但问题出在哪里呢很多教程、脚本、老项目里到处都在用/usr/local这个路径这是Intel时代Homebrew的默认安装位置。如果你在M1上把两个版本的Homebrew都装了终端同时导出/opt/homebrew/bin和/usr/local/bin那就有得折腾了。实际结果就是你输入brew命令时可能用的是这个版本但它内部调用依赖时又跑去另一个目录找最后装出来的包架构混乱。我的做法是主力保留一个原生arm64版Homebrew路径固定在/opt/homebrew只有在明确需要某个Intel版工具并且没有其他替代方案时才会临时在Rosetta终端里用/usr/local那个Homebrew在shell配置里不要同时把两个bin目录加入PATH避免不可控的冲突。判断当前终端里的brew到底是什么版本可以这样查# 查看brew的架构信息 brew config | grep -i rosetta # 查看brew安装路径 which brew # /opt/homebrew/bin/brew 说明是原生arm64版本这里有一个细节值得注意如果你在普通终端里通过Rosetta 2的方式打开过shell又执行过brew安装操作那么某些包可能已经被装成x86_64版了。用file命令逐一看一下已经安装的包for f in $(brew --prefix)/bin/*; do file $f | grep -q x86_64 echo $f 是Intel版; done如果发现大量不需要的x86_64残留建议清理后重装或者直接卸载Intel版Homebrew否则后面你做UI自动化时某个底层依赖就可能是全链路里唯一的“Intel钉子户”。2.2 JDK和Node的版本选择别为了追新给自己埋雷UI自动化链路里JDK的存在感不那么强但它确实是一个底层依赖尤其是Android方向的自动化。Gradle在构建测试项目时要检查JDK版本Appium的UiAutomator2服务也会依赖Java环境。如果在M1上装的是Intel版或者太新的JDK报错的时候你甚至不会第一时间联想到它。我建议直接装Zulu的Apple Silicon版本这是目前兼容性比较稳的选择。最好选JDK 17长期支持版本而且Android生态里Gradle和AGP对17的支持已经很成熟了。# 安装Zulu JDK 17这是针对Apple Silicon编译的原生版本 brew install --cask zulu17 # 验证安装结果 java -version # 输出应包含17.x且file查看应为arm64 file $(which java)Node.js也一样直接用原生arm64版本。最简单的方式就是brew install node它会自动装到/opt/homebrew。不过npm的全局安装权限问题在M1上依然存在如果你用系统自带的node路径npm install -g会频繁报权限错误。我个人的推荐是用nvm来管理Node版本这样既避免权限问题又方便切换# 安装nvm后安装LTS版本Node nvm install --lts nvm use --lts补充一个容易忽视的点Node的某些依赖包在安装时要编译原生模块M1上编译环境不完整会直接失败。所以首先要确保已经安装了完整的Xcode Command Line Toolsxcode-select --install这一步最好放在装Node之前否则中途报错再去装还得把依赖重新装一遍。2.3 环境变量一次配齐别等报错再来改UI自动化需要的环境变量主要是JAVA_HOME、ANDROID_HOME以及PATH里的platform-tools和emulator目录。很多人习惯把环境变量写进~/.zshrc但我后来发现这是个坑某些后台启动的进程、IDE集成的终端、以及CI环境并不会读取~/.zshrc它们用是~/.zprofile或者系统级的环境配置。所以我的实际建议是全局性的环境变量写在~/.zprofile里终端会话相关的别名和便捷配置写在~/.zshrc里。这样不管你从哪个入口启动UI自动化任务环境变量都在。# ~/.zprofile 中追加以下内容 # 使用java_home工具动态获取JDK路径这样以后升级JDK不会写死 export JAVA_HOME$(/usr/libexec/java_home -v 17) # Android SDK的默认路径 export ANDROID_HOME$HOME/Library/Android/sdk # 把platform-tools和emulator加入PATH export PATH$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH为什么不用写死路径而用/usr/libexec/java_home因为M1环境下通过brew安装cask版本的JDK后实际安装路径可能随版本号变化硬编码一个具体路径下次某个工具自动升级JDK就把路径带偏了。java_home是macOS自带的查询工具动态返回正确的JDK目录这个做法在Intel时代就这样用到了M1依然可靠。配完之后验证一下source ~/.zprofile echo $JAVA_HOME adb version emulator -version这两条命令能输出版本信息说明Android SDK的链路基本通了。3. 移动端自动化这条主线Appium 2.x在M1上的完整链路3.1 Appium 2的Driver机制改变了什么Appium从2.x版本开始把iOS和Android的支持拆成了独立driver不再像1.x那样内置所有功能。这意味着你安装了Appium主程序之后必须手动安装对应平台的driver否则启动服务时会报找不到驱动。# 全局安装Appium 2 npm install -g appium # 安装Android驱动 appium driver install uiautomator2 # 安装iOS驱动 appium driver install xcuitest # 查看已安装的驱动 appium driver list # 启动Appium服务指定兼容路径 appium --base-path /wd/host启动时加--base-path /wd/hub这个参数主要是为了兼容旧版测试代码中的URL约定。因为Appium 2默认的base path改了很多老项目的desired capabilities里还写着http://127.0.0.1:4723/wd/hub。如果你不加这个参数老代码就会连不上服务。保持这个参数启动新旧项目都能跑。安装driver这一步在M1上基本没遇到架构问题因为Appium的driver本质上就是npm包只要Node是arm64原生它们就会跑得很好。3.2 Android模拟器的架构选型别再把x86_64镜像当默认选择这是我在M1上踩得最深的一个坑值得单独强调Android模拟器的系统镜像Intel时代早就习惯了下载x86_64架构。到了M1上如果你还用同样的思路去下载x86_64镜像模拟器是可以启动的因为Rosetta 2和Hypervisor.Framework撑住了但实际用起来性能一言难尽而且时不时会出现莫名其妙的崩溃。正确的做法是下载arm64-v8a架构的系统镜像来创建AVD# 先用sdkmanager安装arm64-v8a镜像 sdkmanager system-images;android-34;google_apis;arm64-v8a # 创建AVD avdmanager create avd -n m1test -k system-images;android-34;google_apis;arm64-v8a # 检查虚拟化加速能力 emulator -accel-check这里要注意两个细节emulator -accel-check输出OK说明Hypervisor.Framework正常工作M1上的Android模拟器不需要额外安装HAXMHAXM在Apple Silicon上已经彻底废掉了如果accel-check输出的是超时或失败先检查Mac的安全设置和虚拟化框架是否被占用这个问题通常和Docker或虚拟机软件冲突有关不是模拟器本身坏了。M1上的Android模拟器只要用对了arm64-v8a镜像体验其实是相当不错的。启动速度、操作流畅度都比我在Intel Mac上要好很多这一点算是个意外惊喜。3.3 iOS模拟器与XCUITest的准备Xcode这一步逃不掉在M1上跑iOS的UI自动化基本上绕不开Xcode。不过有一点可以放心M1上的iOS模拟器都是arm64原生架构不需要像Android那样费心选镜像类型。先确认Xcode和模拟器Runtime都已经安装# 查看当前已有模拟器设备 xcrun simctl list devices # 查看可用的iOS Runtime xcrun simctl list runtimes # 启动一个具体模拟器 xcrun simctl boot iPhone 15 # 打开模拟器窗口 open -a Simulator我在实际操作中遇到过一个和架构无关但很坑的问题模拟器Runtime没有下载。Xcode安装后默认可能只带最新版本的iOS Runtime如果你用旧版本Xcode或者系统里同时存在多个Xcode版本xcrun simctl list runtimes会显示缺少某个版本。这时候Appium跑iOS用例时报错信息经常是“unable to find a destination matching”很多人误以为是M1兼容问题其实只是模拟器还没装全。用Xcode的Components界面下载对应版本的iOS Simulator Runtime问题就解决了。这个步骤本身不用特别记忆关键是要知道这个误判方向别在架构层面浪费时间排查。4. Web端容易被忽略的驱动架构Selenium系工具的arm64版本管理4.1 Chrome与chromedriver必须架构一致Rosetta救不了这个Web端的UI自动化看起来比移动端简单无非是Selenium加浏览器驱动但M1上第一个“王炸”往往就是chromedriver。你从某些镜像站随便下载一个chromedriver它很可能是x86_64版而M1上Chrome默认跑的是arm64原生版。Selenium启动Chrome时chromedriver要作为独立进程把自动化指令翻译给浏览器。如果chromedriver和Chrome架构不一致轻则报“session not created”重则“driver service unexpectedly exited”。这里Rosetta 2帮不了忙因为两个进程属于不同的架构世界指令翻译没法弥合进程间的协议通信。正确的路径是下载官方提供的mac_arm64版本chromedriver# 手动方式到官网下载对应Chrome大版本的mac_arm64驱动 # 放到统一管理目录 mkdir -p ~/drivers/chrome mv ~/Downloads/chromedriver ~/drivers/chrome/ chmod x ~/drivers/chrome/chromedriver # 加入PATH中 export PATH$HOME/drivers/chrome:$PATH如果你用Python写Web自动化我更推荐直接在代码里使用webdriver-manager它会在运行时自动检测Chrome版本和系统架构去官方源下载匹配的driverfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome( serviceService(ChromeDriverManager().install()) ) driver.get(https://example.com) driver.quit()这个库还有一个特别的优势它会下载到用户目录下的缓存位置不污染系统PATH。对做多人协作的测试项目来说每个人机器上自动下载对应架构的驱动比在代码仓库里放一个固定版本要省心得多。4.2 火狐和Safari的驱动又是另一回事如果你同时要测Firefoxbrew install geckodriver安装的没问题只要确认它是macos-aarch64版本即可。也可以去官网手动下载文件命名里通常有“aarch64”字样对照着选就好。Safari反而是Web UI自动化里最省事的macOS自带了safaridriver位于/usr/bin/safaridriver不需要额外安装任何驱动。# 启用Safari的远程自动化开关 safaridriver --enable不过提个醒Safari在M1上的自动化功能稳定性不如Chrome日常开发调试用没问题但大规模跑回归时我很少拿Safari做主力Chrome仍然是最稳的Web端自动化浏览器。5. 我实际踩过的三个坑与完整的排查链路5.1 坑一终端里能启动模拟器Appium却报ENOENT某个下午我明明能在终端手动启动Android模拟器但通过Appium跑用例时日志里出现spawn emulator ENOENT意思是Appium进程找不到emulator这个命令。排查链路是这样的先在终端里执行which emulator结果确实指向了$ANDROID_HOME/emulator/emulator手动把模拟器启动起来Appium依旧报同样的错查看Appium的启动方式发现它是从后台服务拉起的进程而这个后台进程的环境变量来自~/.zprofile打开~/.zshrc里面确实配了Android相关环境变量但~/.zprofile是空的于是后台进程根本没有ANDROID_HOME。根因就是环境变量配置位置不对终端能读到不代表所有进程都能读到。把Android相关变量移动或复制到~/.zprofile之后重启Appium服务问题解决。这个坑很典型而且和M1没有直接关系但迁移到M1的用户因为会重配环境特别容易踩到。排查思路值得记一下先用env或printenv看Appium进程实际拿到哪些环境变量再反推配置位置对不对。5.2 坑二XCUITest在模拟器上构建失败误以为是架构问题跑iOS端用例时Appium每次初始化都会用xcodebuild构建WebDriverAgent但第一次构建直接失败日志里说找不到匹配的destination。我当时第一反应是M1的模拟器架构问题于是花了很多时间验证arm64设置。后来才发现问题出在模拟器Runtime缺失。xcrun simctl list runtimes显示只有最新版iOS Runtime而测试代码里指定的设备系统版本没有对应Runtime。Xcode安装时不会把所有版本的模拟器Runtime都下载只装最新版。解决办法很简单在Xcode的Components里下载匹配版本的iOS Simulator Runtime。也可以多试几个模拟器设备名称但更稳妥的还是把Runtime补全。这个坑给我的教训是M1虽然是新架构但并不是所有问题都来自架构。遇到异常先看系统日志和工具自带的输出别把什么都往架构上联想。5.3 坑三Java版本和Gradle兼容性翻车有一次在跑Android原生测试用例时Gradle报错提示“Unsupported class file major version”我当时还没反应过来以为又是驱动问题。后来通过java -version一看系统里默认JDK已经被某个工具自动切换到了Java 21而当前Gradle版本不支持。Android生态对JDK版本要求一直很保守尤其UI自动化的项目经常是老项目Gradle和AGP版本都偏低。一上来装最新JDKGradle直接罢工。解决方式是在项目的gradle.properties里显式指定JDK路径org.gradle.java.home/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home或者在shell里设置JAVA_HOME指向JDK 17后再跑测试。我的建议是不要为了追新在UI自动化环境里装最新JDK17是当前兼容性最好的中间值。6. 从环境搭建到第一个测试用例跑通6.1 一个最小的Web端用例环境配好之后我习惯先用一个最简单的Web用例验证整条链路是否畅通。下面是Python版本的Selenium最小用例from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install())) driver.get(https://example.com) print(driver.title) assert Example in driver.title driver.quit()如果这一步能正常打开浏览器并打印出标题说明Chrome、chromedriver和Selenium之间的架构链路没有问题。后面再复杂的Web自动化都是在这个基础上扩展。6.2 一个最小的Android端用例移动端我用Appium连模拟器跑一个简单的系统设置界面操作。先把模拟器启动好再启动Appium服务然后执行from appium import webdriver caps { platformName: Android, appium:deviceName: m1test, appium:automationName: UiAutomator2, appium:platformVersion: 14.0, } driver webdriver.Remote(http://127.0.0.1:4723/wd/hub, caps) driver.find_element(byxpath, value//*[text设置]).click() driver.quit()第一次连接会比较慢因为Appium需要在模拟器里安装和启动UiAutomator2相关的服务组件。只要模拟器用的是arm64-v8a镜像这一步通常不会出大问题。如果你在这个用例里遇到超时优先检查两方面一是emulator -accel-check是否OK二是模拟器里的网络代理设置某些公司内网环境会对adb和Appium的通信造成干扰。6.3 环境自检清单几分钟内定位问题结合上文所有内容我整理了一份快速自检清单每次搭建完新机器或遇到莫名问题时按顺序过一遍检查项命令期望结果CPU架构uname -marm64Homebrew路径which brew/opt/homebrew/bin/brewJava架构file $(which java)包含arm64Node架构file $(which node)包含arm64Android SDK路径echo $ANDROID_HOMESDK目录存在且非空模拟器加速emulator -accel-checkOKiOS模拟器Runtimexcrun simctl list runtimes已安装需要的版本chromedriver架构file $(which chromedriver)包含arm64这套清单对我来说价值很大因为UI自动化环境出问题时大多数都能归结到架构或环境变量两类原因。脚本化地把检查项过一遍比盲目搜报错日志要高效得多。最后说一条个人体会在M1上搭UI自动化环境和Intel时代最大的区别不是某个工具装不上而是你要时刻具备“架构意识”。每次安装一个组件多问一句“它是原生arm64吗”很多坑根本不会发生。按照本文的顺序一次配好后面就是快乐的测试开发时间了。