出海团队第一次去 App Store Connect 传素材时都会撞上同一个数字:十四个语言 × 六张截图 × 三种设备尺寸,近三百张图。运营同学手工截一遍,一天搭进去还容易漏改状态栏时间。这类重复劳动最适合丢给一台跑 macOS 图形界面的机器批量处理,而不是占用团队里唯一那台 MacBook。
为什么放到云端 Mac mini 上跑
本地笔记本跑截图有两个硬伤:一是模拟器一开就是四五台,风扇狂转还要占着屏幕不能干别的;二是本地环境和发布分支不同步,截图用的代码跟实际提审的版本经常对不上。把这套流程搬到一台独享的 Mac mini 物理节点上,好处很直接:
- 开多少台模拟器都不影响自己手头的开发机;
- 可以固定一个专门用来出素材的分支和 Xcode 版本,截图环境和构建环境彻底分开;
- 任务跑完直接通过 SSH 把结果目录同步下来,不用守在机器前面等。
截图自动化本质是"跑一遍 UI 测试顺便截图",所以前提是应用要能被 UI 测试正常驱动到目标页面,埋点式的强制登录、验证码拦截都要提前做测试环境开关。
用 fastlane snapshot 搭起流水线
先在项目里初始化 snapshot,生成 SnapshotHelper.swift 和 Snapfile:
cd MyApp
fastlane snapshot init
Snapfile 是整个流程的配置核心,把语言列表、设备列表、输出目录都写死在这里,避免每次手动传参数:
devices([
"iPhone 16",
"iPhone 16 Pro Max",
"iPad Pro (12.9-inch)"
])
languages([
"zh-Hans", "zh-Hant", "en-US", "ja",
"ko", "de-DE", "fr-FR", "es-ES",
"pt-BR", "ru", "it", "id",
"th", "vi"
])
scheme("MyAppUITests")
output_directory("./screenshots")
clear_previous_screenshots(true)
override_status_bar(true)
concurrent_simulators(true)
UI 测试里只需要在目标页面调一句截图函数,不用关心多语言逻辑,fastlane 会自动按 languages 数组挨个跑:
func testHomeScreenshot() {
let app = XCUIApplication()
app.launch()
snapshot("01Home")
}
多模拟器并发与资源边界
concurrent_simulators(true) 打开后,fastlane 会按设备数量自动创建对应的模拟器实例。但"能开"不代表"该同时开这么多",内存是真正的瓶颈。以云端 Mac mini 的两档配置为例:
| 机型 | 内存 | 建议并发模拟器数 | 完整跑一轮(14 语言 × 3 设备)耗时参考 |
|---|---|---|---|
| Mini Basic(M4/16GB) | 16GB | 3~4 台 | 约 55~70 分钟 |
| Mini Standard(M4/24GB) | 24GB | 5~6 台 | 约 35~45 分钟 |
超过建议并发数后,系统会因为内存压力把模拟器进程换出到磁盘,截图任务反而会排队变慢,还容易触发个别语言截图失败需要重跑。稳妥做法是用 xcrun simctl list devices 先确认当前已启动的模拟器数量,超过阈值就分批执行:
xcrun simctl list devices booted
fastlane snapshot --devices "iPhone 16" --languages "zh-Hans,ja,ko"
fastlane snapshot --devices "iPhone 16" --languages "en-US,de-DE,fr-FR"
命名规范与 frameit 加框
跑完一轮会得到几百张图,命名混乱是后续上传 App Store Connect 时最容易出错的环节。建议输出目录按 语言代码/设备名/序号-页面名.png 三层结构存放,和 Snapfile 里的默认行为保持一致,不要自己再改一层命名。
如果截图需要带设备边框展示(常见于宣传页而不是 App Store 商店页本身),用 frameit:
fastlane frameit
它会读取截图元数据自动匹配对应机型的边框,批量输出到 screenshots/framed 目录,不需要在 Photoshop 里一张张对齐。
常见坑与排查
- 状态栏没统一:漏配
override_status_bar(true)会导致截图里的时间、电量、信号格各不一样,审核团队肉眼就能看出是真机随手截的。 - 系统字体缺失导致排版乱:部分语言(泰语、阿拉伯语)在旧版 iOS 模拟器上渲染行高异常,升级到最新 Xcode 附带的模拟器运行时通常能解决。
- 键盘语言残留:UI 测试如果涉及输入框,记得在测试前清理模拟器的键盘设置,否则某些语言截图里会带出上一次残留的输入面板。
- 模拟器状态未复位:多轮测试之间建议
xcrun simctl erase all清空所有模拟器数据,避免登录态、缓存内容串到下一语言的截图里。
CI 集成:定时增量出图
素材不需要每次发版都全量重截,只有 UI 改动的页面才需要更新对应截图。做法是在云端 Mac mini 上跑一个定时任务,对比 Git diff 涉及的页面,只重跑相关的 UI 测试用例:
git diff --name-only HEAD~1 HEAD | grep "Screens/" > changed_screens.txt
配合 self-hosted runner 把这段逻辑接进 GitHub Actions,PR 合并到发布分支时自动触发增量截图,再由人工确认无误后统一上传。整套流程搬到独享物理节点上跑,不会和团队其他人的构建任务抢资源,也不用为了截图专门占用一台开发机。
常见问题
一次跑多少个模拟器比较合适?
M4 芯片、16GB 内存的机型建议同时开 3~4 台模拟器截图,超过这个数字截图任务会因内存压力而排队变慢,不如分两批跑更快。
截图里的状态栏时间和电量怎么统一?
在 Snapfile 里设置 override_status_bar true,fastlane 会自动把时间改成 9:41、电量改成满格,不需要手动截屏后再修图。