在云端 Mac 上建立 Swift 6 并发迁移门禁

在云端 Mac 上建立 Swift 6 并发迁移门禁

一个维护多年的 iOS 工程切到 Swift 6 时,最危险的做法不是改得慢,而是一次打开语言模式后才发现数百条并发诊断混在业务改动里。更稳妥的路径是在云端 Mac 上先建立独立检查任务:固定工具链、隔离构建目录、记录当前告警,再按模块逐步收紧。

先把迁移拆成两个阶段

第一阶段仍使用 Swift 5 语言模式,但把 SWIFT_STRICT_CONCURRENCY 设为 complete。这一步会暴露跨隔离域传递非 Sendable 类型、主线程隔离调用以及不安全的全局可变状态,却不会立刻引入 Swift 6 的全部语义变化。

第二阶段才把清理完成的模块切换到 Swift 6。迁移顺序建议从依赖少的基础包开始,再处理网络层、数据层和界面层。叶子模块先稳定,上层诊断通常会随之减少。

阶段 语言模式 检查设置 合并条件
发现问题 Swift 5 complete 并发告警不得增加
清理模块 Swift 5 complete 目标模块告警归零
正式切换 Swift 6 默认严格规则 构建与测试全部通过

并发迁移门禁的目标不是第一天消灭全部告警,而是让主分支从第一天起不再新增问题。

固定一次可复现的检查任务

云端 Mac 上的任务必须显式记录 Xcode、Swift 编译器和提交版本。不要复用开发者手工构建留下的 DerivedData,否则旧模块缓存可能掩盖真实依赖关系。每个提交使用独立目录,任务结束后再按保留策略清理。

#!/bin/zsh
set -o pipefail
export LC_ALL=C
root="$PWD/.ci"
derived="$root/DerivedData"
result="$root/ConcurrencyCheck.xcresult"
log="$root/concurrency.log"

rm -rf "$derived" "$result"
mkdir -p "$root"

xcodebuild -version
xcrun swiftc --version
git rev-parse HEAD

xcodebuild \
  -project Example.xcodeproj \
  -scheme Example \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$derived" \
  -resultBundlePath "$result" \
  SWIFT_VERSION=5.0 \
  SWIFT_STRICT_CONCURRENCY=complete \
  build 2>&1 | tee "$log"

build_status=${pipestatus[1]}
exit "$build_status"

projectscheme 和目标平台应由仓库配置提供,不要在多份脚本里重复维护。若工程使用 workspace,则将 -project 换成 -workspace,同时确认共享 scheme 已提交到版本库。

用告警预算接入现有主分支

老项目通常无法立即做到零告警。第一次运行后保存并发诊断数量,把它设为初始预算;后续提交只要超过预算就失败。每修复一组问题,就在同一个合并请求中下调预算。

budget=${CONCURRENCY_WARNING_BUDGET:-0}
count=$(grep -Ec \
  'warning:.*(Sendable|actor-isolated|concurrency-safe)' \
  .ci/concurrency.log || true)

printf 'concurrency warnings: %s, budget: %s
' "$count" "$budget"
test "$count" -le "$budget"

文本匹配适合作为过渡门禁,但需要固定 LC_ALL=C,并在升级 Xcode 时复查诊断措辞。构建结果包应作为失败附件保留,避免只剩一段被截断的终端日志。最终预算降到零后,可以直接禁止任何并发告警。

避免被生成代码污染

若接口客户端或模型由工具生成,应把生成器版本固定在仓库配置中。无法立即修改的外部生成代码可以单独放入目标模块,但不要把整个业务模块从严格检查中排除。否则新的手写代码也会绕过门禁。

按问题类型修复而不是批量静音

看到 Sendable 诊断时,先判断数据是否真的需要跨任务传递。只读配置优先改为值类型快照;带可变缓存的引用类型可放入 actor;必须共享的同步对象则明确封装锁和访问边界。

界面状态应由 @MainActor 隔离到具体类型或方法。不要为了快速通过检查,把整个数据层或所有协议都标成 @MainActor,这会把后台工作隐式拉回主线程,并制造更多跨域调用。

@unchecked Sendable 只适合安全性已由内部锁、不可变设计或串行队列保证的类型。代码审查至少要回答三件事:保护了哪些字段、所有读写是否经过同一边界、未来新增字段如何继续受保护。回答不了,就不应使用该声明。

回调桥接到 async 时还要验证 continuation 只恢复一次。把“没有回调”和“重复回调”都纳入测试,不能只修到编译器不再提示。

分模块切换并保留验收证据

每个模块切换前,先在 Swift 5 complete 模式下做到自身告警归零,再启用 Swift 6。验收至少包含 Debug 构建、Release 构建、单元测试和关键集成测试;仅运行模拟器 Debug 构建不足以发现优化配置或条件编译分支的问题。

建议为每次迁移记录模块名、负责人、切换提交、剩余豁免和结果包路径。若升级 Xcode,应先在独立分支重新建立基线,不要把工具链升级、并发修复和业务功能塞进同一个变更。

迁移完成后仍保留严格检查任务。Swift 6 能提高默认约束,但依赖更新、Objective-C 边界和生成代码仍可能重新引入隔离缺口。稳定的结束状态不是“曾经清零”,而是每次合并都能证明没有回退。

常见问题

是否应该直接把整个工程切换到 Swift 6?

不建议。先在 Swift 5 模式开启 complete 严格并发检查,清理叶子模块和共享状态,再按模块切换到 Swift 6,失败范围更小。

可以用 @unchecked Sendable 快速消除告警吗?

只应在类型内部已有锁、不可变快照或串行隔离,并经过人工审查时使用;同时记录理由和移除条件,不能把它当作批量静音手段。

并发告警门禁如何避免阻塞已有项目?

先记录当前告警数作为预算,只禁止新增;每修复一批就下调预算,最终降到零后再将任何并发告警视为失败。

独享物理 Mac mini

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

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

选择配置并订购