云端 Mac CI 中用 Address Sanitizer 拦截内存错误

云端 Mac CI 中用 Address Sanitizer 拦截内存错误

一个混合了 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 mini

按任务长度选择云端 Mac 租期

两档 M4 配置可按天、周、月或季租用,节点与实际可用信息以控制台实时返回为准。

选择配置并订购