一个混合了 Swift、Objective-C 和 C/C++ 依赖的 iOS 工程,普通测试连续通过,却可能在某条低频路径上发生数组越界、重复释放或释放后使用。把任务搬到云端 Mac 后,不要只重复执行原有测试;更有效的做法是增加一条 Address Sanitizer(ASan)流水线,让内存访问错误直接终止构建,并保留可复现的结果包。
先确定门禁要抓什么
ASan 主要拦截堆栈越界、释放后使用、重复释放及部分无效指针访问。它尤其适合检查 Objective-C 桥接层、图像处理库、数据库封装和手写 C 接口。纯 Swift 代码也能受益,但一次通过不等于不存在内存问题:没有被测试执行的分支不会被检查,泄漏和数据竞争也需要其他工具补充。
建议先选一组稳定、无需人工交互的测试作为门禁入口,不要一开始就把全部 UI 测试塞进来。核心解析器、缓存层和跨语言接口优先级最高。
| 测试层 | 运行频率 | 失败处理 |
|---|---|---|
| 核心单元测试 | 每次合并请求 | 阻止合并 |
| 集成测试 | 主分支或合并请求 | 阻止发布 |
| 完整 UI 测试 | 定时或版本候选阶段 | 建立缺陷并复现 |
Sanitizer 的价值不只是让任务变红,而是让同一输入能够再次触发同一错误,并留下调用栈、测试名和构建环境。
建立独立 Scheme 与目录
在 Xcode 中复制现有测试 Scheme,命名为 App-ASan,并把它设为 Shared。不要直接修改开发者日常使用的 Scheme,否则本地调试、性能测量和 CI 参数会互相污染。Scheme 应提交到:
App.xcodeproj/xcshareddata/xcschemes/App-ASan.xcscheme
每次任务使用独立的 DerivedData。这样可以避免普通构建生成的对象文件被误用于 ASan,也能防止两个并行任务争用索引和中间产物。结果包同样按任务编号分目录保存,而不是反复覆盖固定文件。
固定测试目标
先查看当前可用的模拟器:
xcrun simctl list devices available
CI 中最好固定设备类型与系统主版本,同时允许系统补丁版本随已安装运行时更新。若团队必须验证多个系统版本,应拆成矩阵任务,不要在一条命令里依赖自动选择。
用 xcodebuild 执行检查
下面的脚本使用独立构建目录和结果包。工作区、Scheme 与模拟器名称应改成项目实际值。
#!/bin/zsh
set -u
ROOT="${PWD}"
OUT="${ROOT}/artifacts/asan"
DERIVED="${ROOT}/.ci/DerivedData-ASan"
rm -rf "${OUT}" "${DERIVED}"
mkdir -p "${OUT}"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-ASan \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath "${DERIVED}" \
-resultBundlePath "${OUT}/App-ASan.xcresult" \
-enableAddressSanitizer YES \
CODE_SIGNING_ALLOWED=NO \
>"${OUT}/xcodebuild.log" 2>&1
status=$?
exit ${status}
不要给测试命令追加无条件成功的 || true。若 CI 平台需要后处理日志,应先保存退出码,完成归档后再原样返回。CODE_SIGNING_ALLOWED=NO 适合模拟器测试;涉及真实设备或归档的任务应使用独立签名配置,不能照搬。
ASan 会增加内存占用和执行时间,因此同一台机器上的并发数应低于普通单元测试。出现进程被系统终止时,先降低并发,再判断是否为被测应用缺陷。
让失败证据可以交接
任务失败至少保存 xcodebuild.log 与 .xcresult。前者便于搜索 Sanitizer 报告,后者包含测试层级、失败附件和活动日志。归档前可先做一次完整性检查:
test -s artifacts/asan/xcodebuild.log
test -d artifacts/asan/App-ASan.xcresult
xcrun xcresulttool get test-results summary \
--path artifacts/asan/App-ASan.xcresult \
> artifacts/asan/summary.json
不同 Xcode 版本的 xcresulttool 子命令可能变化,所以应把工具调用与 Xcode 版本一起维护。命令不兼容时,不能因此把原始结果包删除。
排查报告时先找 ERROR: AddressSanitizer,再记录错误类型、首个业务栈帧、触发测试和输入数据。系统库顶部栈帧通常只是崩溃发生的位置,真正的写坏动作可能更早。若错误无法稳定重现,先单独循环对应测试,并禁用随机测试顺序,避免同时改变多个变量。
分层运行并处理常见误区
日常门禁可只跑高风险测试集合,主分支再执行完整 ASan 套件。测试标签或 Test Plan 比按文件名拼接命令更容易维护。新增 C 接口、指针运算或不安全缓冲区处理时,应同步把相关测试加入快速集合。
常见误区有三类。第一,复用普通构建缓存,导致插桩不完整;第二,只上传文本日志,不保存结果包;第三,把 ASan 失败当作偶发测试直接重试。重试可以确认稳定性,但第一次报告仍应保留,连续重试通过也不能自动放行已捕获的非法访问。
最后把验收条件写清楚:测试进程退出码为零、结果包存在、日志中没有 ASan 错误,并且任务未因超时或资源压力被中止。这样门禁判断的是完整执行结果,而不是一行看似成功的测试摘要。
常见问题
Address Sanitizer 是否应该在每次提交中运行?
核心单元测试和高风险模块可以每次运行;耗时较长的完整 UI 测试更适合按主分支、合并请求或定时任务分层执行。
Address Sanitizer 通过是否代表应用没有内存问题?
不代表。它只能发现本次执行路径触发的特定内存访问错误,仍需扩大测试覆盖,并配合泄漏检查、数据竞争检查和真实设备验证。
按任务长度选择云端 Mac 租期
两档 M4 配置可按天、周、月或季租用,节点与实际可用信息以控制台实时返回为准。