惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
Apple Machine Learning Research
Apple Machine Learning Research
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
博客园 - 聂微东
罗磊的独立博客
W
WeLiveSecurity
博客园_首页
Scott Helme
Scott Helme
V
Visual Studio Blog
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Latest news
Latest news
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
About on SuperTechFans
F
Full Disclosure
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 司徒正美
博客园 - Franky
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
阮一峰的网络日志
阮一峰的网络日志
S
Schneier on Security
雷峰网
雷峰网
博客园 - 【当耐特】
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
Engineering at Meta
Engineering at Meta
aimingoo的专栏
aimingoo的专栏
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
T
Tor Project blog
V
V2EX
爱范儿
爱范儿
C
Check Point Blog
T
Threatpost
Project Zero
Project Zero
量子位
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
I
Intezer
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com

肘子的 Swift 记事本 | Fatbobman's Blog

当每一次写入都有了新价格 -- 肘子的 Swift 周报 #144 当 Linux 成为“空气”:容器、Agent 与不再重要的“桌面之争” -- 肘子的 Swift 周报 #143 两个 SwiftUI 动画 Bug 排查小记 SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142 Swift 还让你 Excited 吗? -- 肘子的 Swift 周报 #141 从 Size Class 到可用空间,horizontalSizeClass 还可靠吗? WWDC 26:AI 帮你看完了,然后呢? -- 肘子的 Swift 周报 #140 WWDC 2026 初印象:符合预期,但更务实 -- 肘子的 Swift 周报 #139 Core Data + Observation:从属性级响应到心智解放 稳定 > 新功能 -- 肘子的 Swift 周报 #138 用自定义 Layout 化解 SwiftUI List 的行高与间距跳变 从社区路标到生态基石:Dave Verwer 的新篇章 -- 肘子的 Swift 周报 #137 消失的 WWDC 愿望单 -- 肘子的 Swift 周报 #136 CocoaPods 正在退场,SwiftPM 才刚到第二章 -- 肘子的 Swift 周报 #135 让 AI 从称手到称心 -- 肘子的 Swift 周报 #134 Swift 并发正被更广泛地接纳 -- 肘子的 Swift 周报 #133 从 OpenSwiftUI 到 DanceUI:换个方式 Dive SwiftUI -- 肘子的 Swift 周报 #132 被 Vibe 摧毁的版权壁垒,与开发者的新护城河 -- 肘子的 Swift 周报 #131 苹果的罕见妥协:当高危漏洞遇上“拒升”潮 -- 肘子的 Swift 周报 #130 一墙之隔,不同的时空 -- 肘子的 Swift 周报 #129 我的 App 审核被卡了? -- 肘子的 Swift 周报 #128 CDE:一次让 Core Data 更像现代 Swift 的尝试 50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127 2026 年,为什么我仍在思考 Core Data Macbook Neo:苹果重回校园的起点 -- 肘子的 Swift 周报 #126 Xcode 26.3 + Claude Agent:模型替换、MCP、Skill 与自适应配置 macOS 录屏软件开发实录:从像素抓取到元数据重现 isolated(any) 与 #isolation:让 Swift 闭包自动继承隔离域 在 tvOS 上活下來:一個非典型播放器的工程實錄 告别“可移植汇编”:我已让 Swift 在 MCU 上运行七年 独立开发者的试炼:Zipic 从 0 到 1 的产品化之路 逃离 Mac App Store:如何从零构建独立应用的分发与售卖体系 为什么父视图无法修改子视图的 @State Partytown 实战:修复 GTM 自定义事件与 dataLayer 失效问题 Swift 6 编译加速:启用 Swift-Syntax 预构建 (Prebuilts) Swift 模块化:使用 package 关键字实现 Target 间安全共享 Monorepo 实战:解决 VSCode/Cursor 中 SwiftLint 配置失效问题 VSCode/Cursor 测试 Core Data:手动编译 xcdatamodeld 指南
跨域传递 NSManagedObjectContext 为什么在 Swift 6.2 中不再报错?真正的变化不在编译器
东坡肘子 · 2026-03-04 · via 肘子的 Swift 记事本 | Fatbobman's Blog

当同一段与并发有关的代码在 Xcode 16 中无法通过,却能在 Xcode 26 中顺利编译时,你第一时间会想到什么?我最初的判断是编译器进化了,但现实并没有这么简单。本文将记录我最近遇到的一次有意思的排查过程:从测试失败出发,一步步追到 Core Data 的 SDK interface,最终发现,问题的关键并不完全在 Swift 编译器本身,而在 NSManagedObjectContext 被导入 Swift 的方式已经发生了变化。

问题

最近在为 Persistent History TrackingKit 2 补充和调整测试代码时,我遇到了一个特殊情况,为了模拟两个不同角色(例如主程序和扩展、两个不同进程中的组件)之间通过 Persistent History 合并变更的场景,我在测试中让两个不同域共同引用同一个 NSManagedObjectContext 实例:

let app2Handler = TestAppDataHandler( // actor domain
  container: container,
  context: app2Context,
  viewName: "App2Handler"
)

let kit = PersistentHistoryTrackingKit( // current domain
  container: container,
  contexts: [app2Context],
  currentAuthor: "App2",
  allAuthors: ["App1", "App2"],
  userDefaults: userDefaults,
  uniqueString: uniqueString,
  logLevel: 0,
  autoStart: false
)

完整的测试用例(包括 Actor 隔离与自定义执行器的实现)可查看实际的测试代码

上面的代码,在 Swift 6 模式下, Xcode 26.3(Swift 6.2.4)下可以正常编译,但在 Xcode 16.4(Swift 6.1.2)下却会直接被编译器拦住,给出典型并发安全错误:

sending 'app2Context' risks causing data races

根据我的最初理解,在 Swift 6.1.2 中,编译器认为这里存在风险:

  • app2Context 被送入了一个 actor 相关的隔离域
  • 当前作用域随后又继续使用了同一个 app2Context
  • 因此可能存在 “actor-isolated and local nonisolated uses” 的重叠访问

但从我的具体实现上看,与 context 有关的并发操作都是在其私有队列中进行的;而 TestAppDataHandler 又通过自定义 executor 将 actor 执行固定在这个 context 的执行环境中。从运行时语义上,这段代码理论上是安全的。

起初,我的第一反应是 Swift 6.2 对于 Region-Based Isolation(SE-0414)相关实现更成熟了,因此对 non-Sendable 值跨域传递的分析更准确,放过了 Swift 6.1 中的保守误报,为此我尝试在 6.2 版本中做进一步验证。

Swift 6.2 的并发分析进步了?

为了验证这一点,我先尝试构造一个不依赖 Core Data 的纯 Swift 版本重现。思路很简单:

  • 准备一个普通的 non-Sendable 引用类型
  • 把它传给一个 actor 的初始化器
  • 然后在当前作用域继续访问它
final class Token {
  var value: Int = 0
}

private actor TokenActor {
  init(token: Token) {}
}

func repro() {
  let token = Token()
  let actor = TokenActor(token: token)
  _ = token.value
  _ = actor
}

结果很快就推翻了我最初的判断:

  • Swift 6.1.2:报错
  • Swift 6.2.4:仍然报错

也就是说,Swift 6.2 并没有普遍放过“non-Sendable 值进入 actor 域后,当前作用域继续访问”的模式。

如果只是一般的 non-Sendable 引用类型,这两版编译器的判断是一致的。

在尝试了各种不同的 Token 类型设计后,我将目标逐渐锁定到 NSManagedObjectContext 这个类型本身,或许它有什么特别之处。

通过真实代码来锁定问题

既然不是一般的 non-Sendable 值,那我就反过来,从真实测试代码中持续删减,直到得到能稳定复现差异的最小骨架。

最后得到的版本只剩下这几行:

import CoreData

private actor ReducedContextActor {
  init(context: NSManagedObjectContext) {}
}

func repro() {
  let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  let actor = ReducedContextActor(context: context)
  context.name = "StillLocal"
  _ = actor
}

具体结果如下:

  • Swift 6.1.2:报错
  • Swift 6.2.4:通过

也就是说,此时我们已经拿到了一个非常干净的 Core Data 特定重现。

在 Core Data 的 interface 中找寻答案

根据我之前对于 Core Data 的理解,只有 NSManagedObjectIDNSPersistentContainerNSPersistentStoreCoordinator 被声明为了 Sendable,难道 Xcode 26 中的 Core Data 有了其他的变化?

于是我开始对比 Xcode 16 和 26 自带的 Core Data 头文件和 Swift interface,很快,我就找到了决定性的差异。

在 Xcode 26.3 的 NSManagedObjectContext.h 中,类声明前多了这样一行:

NS_SWIFT_NONISOLATED NS_SWIFT_SENDABLE
@interface NSManagedObjectContext : NSObject <NSCoding, NSLocking>

而在 Xcode 16.4 的对应头文件中,这两个标记是不存在的。

除此之外,在 26.3 的 CoreData.swiftinterface 中,还能看到一系列新增的并发相关导入结果,例如:

convenience nonisolated public init(_ type: CoreData.NSManagedObjectContext.ConcurrencyType)
@preconcurrency nonisolated public func performAndWait<T>(...)
@preconcurrency nonisolated public func perform<T>(...)
nonisolated public func fetch<T>(...)
nonisolated public func count<T>(...)

这与 16.4 版本相比差异非常明显。

到这里,问题的答案已经基本浮出水面了:NSManagedObjectContext 在新 SDK 中被以更强的并发语义导入到了 Swift 中。

NS_SWIFT_SENDABLE 和 NS_SWIFT_NONISOLATED 到底是什么

为了避免凭感觉下结论,我继续追到了宏定义本身。

它们定义在 Foundation 的 NSObjCRuntime.h 中,对应定义如下:

// Indicates that the thing it is applied to should be imported as 'Sendable' in Swift:
// * Type declarations are imported into Swift with a 'Sendable' conformance.
// * Block parameters are imported into Swift with an '@Sendable' function type. (Write it in the same place you would put 'NS_NOESCAPE'.)
// * 'id' parameters are imported into Swift as 'Sendable', not 'Any'.
// * Other object-type parameters are imported into Swift with an '& Sendable' requirement.
#define NS_SWIFT_SENDABLE __attribute__((swift_attr("@Sendable")))

// Indicates that a specific member of an 'NS_SWIFT_UI_ACTOR'-isolated type is "threadsafe" and should be callable from outside the main actor.
#define NS_SWIFT_NONISOLATED __attribute__((swift_attr("nonisolated")))

如果继续往下看,紧接着还有:

// Indicates that a specific member of an 'NS_SWIFT_UI_ACTOR'-isolated type does its own data isolation management and does not participate in Swift concurrency checking.
#define NS_SWIFT_NONISOLATED_UNSAFE __attribute__((swift_attr("nonisolated(unsafe)")))

头文件中的注释给出了相当明确的说明。

  • NS_SWIFT_SENDABLE:它表示被标注的声明应当以 Sendable 的方式导入到 Swift 中。对于类型声明而言,效果就是:这个 Objective-C 类型在导入 Swift 时具有 Sendable 语义;对于 block 参数而言,效果就是:对应的 Swift 闭包会被导入为 @Sendable

  • NS_SWIFT_NONISOLATED:它表示被标注的成员应当以 nonisolated 的方式导入到 Swift 中。

    在注释里,这个宏被描述为针对某个 member 的标记。不过在 NSManagedObjectContext 上,Apple 实际上把它放在了类声明前,这意味着至少在导入器层面,它对整个类型的导入行为产生了更广泛的影响。

而实际导入结果已经表明:NSManagedObjectContext 在 Xcode 26 对应的 SDK 中,不再被当作一个普通的 non-Sendable Objective-C 类来处理。它同 NSManagedObjectIDNSPersistentContainerNSPersistentStoreCoordinator 一样现在都是 Sendable 类型了。

把这两个宏用到我们自己的 Objective-C 类上

尽管我们已经接近答案了,但我还是希望做最后一步验证:如果把 NS_SWIFT_SENDABLENS_SWIFT_NONISOLATED 挂到我自己的 Objective-C 类上,Swift 的行为会不会跟着变化?

为此我构造了两个本地 Objective-C 模块:

未标注版本

@interface ObjCPlainToken : NSObject
@property (nonatomic) NSInteger value;
@end

标注版本

NS_SWIFT_NONISOLATED NS_SWIFT_SENDABLE
@interface ObjCAnnotatedToken : NSObject
@property (nonatomic) NSInteger value;
@end

Swift 测试代码两边都保持同样的结构:

private actor TokenActor {
  init(token: ObjC...Token) {}
}

func repro() {
  let token = ObjC...Token()
  let actor = TokenActor(token: token)
  token.value = 1
  _ = actor
}

实验结果非常干脆:

  • 未标注版本
    • Swift 6.1.2:报错
    • Swift 6.2.4:报错
  • 标注版本
    • Swift 6.1.2:通过
    • Swift 6.2.4:通过

这一步几乎可以视为实锤:这两个宏确实能够改变这类诊断的结果。

NSManagedObjectContext 在新 SDK 中恰好获得了这两个标记。NSManagedObjectIDNSPersistentContainerNSPersistentStoreCoordinator 至少从 Xcode 16 时便已经使用了 NS_SWIFT_SENDABLE,但并没有像 NSManagedObjectContext 这样同时获得 NS_SWIFT_NONISOLATED

到这里,我们该如何重新理解最初的问题

现在可以给出一个更准确的结论了。

最初我把问题理解为:

  • Swift 6.1 过于保守
  • Swift 6.2 更聪明,因此放过了 NSManagedObjectContext

现在看来,更准确的说法应该是:

  1. 一般的 non-Sendable 引用类型,Swift 6.1 和 Swift 6.2 对这种模式的态度是一致的,都会报错。
  2. NSManagedObjectContext 在 Xcode 26 的 SDK 中,被以 NS_SWIFT_SENDABLENS_SWIFT_NONISOLATED 等更强的并发语义导入到了 Swift。
  3. 因此它在 Xcode 26 的 SDK 中不再是一个 non-Sendable Objective-C 类型而是一个 Sendable 类型。

看似是 Swift 编译器的进化,真正发生变化的,其实是框架的导入语义

结语

尽管 NSManagedObjectContext 现在可以跨域传递,但这并不意味着在生产环境中我们也应该这样使用。从 SwiftData 的 interface 来看,ModelContext 虽然有一个 @unchecked Sendable 扩展,但该扩展同时被标记为 unavailable,并明确写了 contexts cannot be shared across concurrency contexts。除非确实遇到需要跨域的场景,还是应该尽量遵循 Core Data 一贯的并发编码准则。

如果我在发现问题后第一时间检查 NSManagedObjectContext 的声明变化,或许很快就能查明真相。但正是因为先入为主地把问题归因于编译器,才有了这次一路追到 SDK 头文件的过程。这次经历也给了我一个提醒:当新旧版本的编译行为出现差异时,除了考虑编译器本身的变化,也值得去检查框架的导入语义是否同步发生了调整。很多时候,答案藏在你没有第一时间想到去看的地方。