你的测试在Mac上全绿,到GitHub Actions上全红——如果这听起来耳熟,问题可能不是你以为的那样。
我们花了三次错误假设和无数张差异对比图,才找到真正的原因。测试时间从本地170秒降到7秒,CI从30分钟压缩到17分钟。这条路径值得复盘。
![]()
从模拟器到macOS:一次25倍提速的迁移
我们的测试套件原本跑在iOS模拟器上。每次xcodebuild test都要启动模拟器、等待就绪、部署测试包、执行——完整跑一遍170秒。本地开发还能忍,CI上就是烧钱:付费的macOS runner干等着虚拟iPhone开机。
于是我们开始审计:多少测试真的需要模拟器?
答案是几乎不需要。我们的应用逻辑——状态管理、数据解析、网络处理、导航——都是纯Swift,不调用UIKit。而SwiftUI视图?通过NSHostingView在macOS上渲染完全没问题,苹果自己的框架负责跨平台转换。
我们把目标平台从iOS Simulator切到macOS,重新跑套件。大部分测试直接通过,少数需要#if os(iOS)保护——比如UIImage处理、CLAuthorizationStatus这些真正依赖iOS API的部分。这些留在模拟器,其余全部迁移。
结果:7秒。同样的测试,同样的断言,25倍提速。CI上更夸张——我们改成"构建一次"模式(构建测试目标、上传构建产物、然后并行分发测试任务用xcodebuild test-without-building)。总耗时从约30分钟降到约17分钟。
逻辑测试在macOS上完美运行。截图测试没有。
第一次假设:分辨率不匹配
Retina Mac渲染2x,CI虚拟机(GitHub Actions的macOS runner)渲染1x。我们写了一个自定义渲染策略,把位图钉死在固定尺寸——390x844,1x缩放。这解决了尺寸对不上问题,测试还是失败。
第二次假设:字体渲染差异
物理Mac和CI虚拟机的字体确实有细微差别——相同视图大约95%像素匹配。我们把精度阈值降到95%,测试通过。但这感觉不对:降到85%就不是在测UI了,真正的回归会藏在噪声里。
我们卡住了。尺寸固定了,精度妥协了,还是找不到根因。
第三次假设:颜色空间
然后注意到一个细节:差异图里有些像素不是内容区别,是亮度区别。sRGB?Display P3?CI虚拟机用的颜色空间和开发机不同。
我们强制指定NSBitmapImageRep的颜色空间为sRGB,测试通过率跳升,但没到100%。还有东西没抓到。
真正的元凶:抗锯齿的随机性
最后对比两张"应该相同"的截图,放大到像素级——边缘像素的Alpha值有±1差异。不是颜色空间,不是分辨率,是Core Graphics的抗锯齿在物理机和虚拟机用了不同算法或随机种子。
我们改了渲染策略:关闭抗锯齿,或者把视图渲染到固定倍数的离屏缓冲区再下采样。最终方案是后者——先渲染到2x缓冲区,再用Lanczos重采样到1x。这样既保留了视觉锐度,又消除了平台间的抗锯齿差异。
测试全绿。精度阈值回到99%,CI和本地完全一致。
为什么这个修复值得分享
常见的两种 workaround 都有隐性成本。"在CI上录制基准图"意味着你没法本地验证截图——每次UI改动变成多步仪式:推代码、等CI失败、从产物里下载新PNG、提交、再推、再等。改10个视图就是10张盲提的图,两个人同时改UI还会遇到二进制PNG的合并冲突。
"降低精度阈值"更危险。95%听起来够严格,但UI回归往往就藏在剩下的5%里。我们试过降到85%,确实全绿了,但也意味着按钮错位2像素、文字截断1行都会被忽略。
真正的解决需要理解渲染管路的完整链条:几何→光栅化→抗锯齿→颜色空间→下采样。每个环节都可能是平台差异的来源。
迁移到macOS测试的长期收益
除了速度,这次迁移带来几个意外收获。
测试可靠性提升。模拟器偶尔卡死或启动超时,macOS原生进程稳定得多。CI失败率从每周几次降到几乎为零。
调试体验改善。失败时可以直接在开发机复现,不需要ssh进CI环境或下载产物。截图文件用Quick Look就能对比。
资源成本下降。GitHub Actions的macOS runner按分钟计费,17分钟 vs 30分钟是直接的成本优化。更隐蔽的收益是并行度——因为单任务更快,同样的并发配额能覆盖更多PR。
给正在考虑类似迁移的团队
先审计测试的真实依赖。我们的经验是:纯Swift逻辑、SwiftUI视图、大部分单元测试都可以迁移;涉及UIKit特定API、硬件传感器、推送通知的需要保留在模拟器。用#if os(iOS)做条件编译,保持单一代码库。
截图测试需要额外投入。不要假设"macOS和iOS渲染一样"——几何一样,但光栅化细节可能不同。预留时间处理抗锯齿、字体、颜色空间的平台差异。
考虑渲染策略的抽象。我们把截图生成封装成可配置的策略对象:目标尺寸、缩放倍数、重采样算法、颜色空间、精度阈值。这样同一套测试可以在不同环境用不同参数,而不改测试代码本身。
CI流程重新设计。test-without-building模式需要把构建和测试解耦,产物传递用GitHub Actions的upload-artifact/download-artifact。并行任务数根据截图测试数量调整——我们按模块拆分,每个模块几百张图,单任务2-3分钟。
数据收束
这次修复的量化结果:本地测试170秒→7秒(25倍),CI总耗时30分钟→17分钟(43%),精度阈值95%→99%同时保持跨平台一致,CI失败率从每周数次趋近于零。500+截图测试现在本地和CI完全同步,不再需要"在CI上录制基准图"的妥协流程。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.