将来的自己,会感谢现在努力的自己!

0%

深度剖析OC属性修饰符:atomic与nonatomic的真正区别

深度剖析OC属性修饰符:atomicnonatomic 的真正区别

作为一名 iOS 开发者,你一定在无数个 .h 文件中敲下过 @property (nonatomic, strong) ...。你也一定知道,如果不写 nonatomic,属性默认就是 atomic。但在现代 iOS 开发的语境下,atomic 几乎已经从我们的代码里销声匿迹。这背后究竟隐藏着怎样的技术现实?本文将从内存模型、汇编指令到线程安全逻辑,严谨而完整地为你理清这对修饰符的本质差异。


1. 基础定义:编译器为属性生成了什么?

atomicnonatomic 的核心区别在于:编译器是否为 getter 和 setter 方法自动生成互斥锁

  • atomic(原子性):编译器会生成一个自旋锁(历史上是 spinlock,现已优化为更现代的锁),包裹整个 getter / setter,保证单次读/写操作的原子性。
  • nonatomic(非原子性):不生成任何锁,直接进行内存访问。

简单来说,atomicnonatomic 多了一层同步保障。但这一层保障到底保护了什么?很多人在这里开始产生误解。


2. “数据完整性”的真实含义:撕裂写入与单指令原子性

你可能听过这样一句话:“atomic 能够保证数据的完整性。” 这里的“完整性”并不是指数据本身“正确”,也不是文件系统里的数据损坏,而是指在并发读写时,防止你读到一份“写了一半”的、新旧二进制位混杂的无效临时状态

要理解这一点,我们必须深入到 CPU 指令与内存宽度的层面。

2.1 单条指令就能完成的读写——nonatomic 也是完整的

在如今 64 位的 ARM 架构下,对于指针以及所有宽度小于等于 64 位的标量类型(如 intlongdoubleBOOL 等),CPU 的 load / store 指令本身就是原子的。即使你修饰为 nonatomic,一个线程正在写入,另一个线程读取,读到的值必定是一个完整的旧值或完整的新值,绝不会出现“半个整数”这种荒唐的结果。

对于这类属性,atomicnonatomic内存读写的完整性上没有任何区别atomic 的锁在这里是纯粹的性能开销,提供不了额外价值。

2.2 需要多条指令才能完成的结构体——atomic 真正的战场

当属性的类型内存占用超过了 CPU 单次原子访存的宽度时,nonatomic 就会产生所谓的“撕裂写入”(Torn Write)或“撕裂读取”。

最典型的例子是 CGRect。在 ARM64 下,CGRect 包含 4 个 CGFloat(即 double,共 32 字节)。给一个 CGRect 属性赋值,永远不可能是一条指令完成。

假设有一个被声明为 nonatomic 的属性:

1
@property (nonatomic, assign) CGRect hugeFrame;

现在考虑以下并发场景:

  1. 线程 A 正在执行 self.hugeFrame = newRect;。这个写入被拆分成多条指令,逐部分将 32 个字节拷贝进内存。假设它刚写完前 16 个字节(旧的 origin),还有 16 个字节(新的 size)未写。
  2. 恰在此时,线程 B 执行 CGRect myRect = self.hugeFrame;
  3. 线程 B 读到的 myRect,其 origin 来自旧值,而 size 来自新值。这成了一个在开发者的语义里从未存在过的、一半旧一半新拼凑出来的 CGRect

这个拼凑出来的值,就是丧失“数据完整性”的典型产物。此时,atomic 的作用就得以体现:它通过加锁,强制整个 getter / setter 的代码块作为一个不可分割的整体来执行,保证绝不出现这种新旧混杂的临时状态

结论修正公式: atomic 只保证在读写像 CGRect 这类需要多条指令才能完成的大内存结构时,不会出现新旧值拼凑的无效状态;对于标量、指针等单指令类型,它不提供额外的内存完整性收益。


3. 多维对比:一张表看清全部差异

对比维度 atomic (原子性) nonatomic (非原子性)
互斥锁 ✅ 自动生成。getter/setter 被互斥锁包裹。 ❌ 不生成任何锁。纯裸内存操作。
对单指令类型的完整性 完整,但锁是多余的。 完整。指令本身就是原子的。
对多指令复合类型的完整性 ✅ 能防止撕裂写/读,保证你读到单一完整的旧值或新值。 ❌ 可能产生新旧值拼凑的无效数据。
性能 。频繁加锁/解锁的开销巨大。竞争激烈时可能比 nonatomic 慢 20 倍以上。 极致。没有任何额外的软件同步开销,直接访存。
默认行为 Objective-C 的默认选项。若不显式书写 nonatomic,属性即为 atomic 必须显式书写。但几乎是所有现代 iOS 项目的事实标准。

4. 核心误区:atomic 绝不是线程安全

这是整个讨论里最关键、也最容易被误解的一点。Apple 官方文档早已明确强调:

Atomicity has to do with the integrity of the property’s value, not with the thread-safety of your object’s logic.

atomic 属性只保护了这个属性本身的一次读或一次写操作不被破坏,但它与你的应用逻辑是否线程安全几乎是两回事。

4.1 复合操作无法保护

假设有一个 atomicNSInteger 属性 count,你执行了这样一段代码:

1
self.count = self.count + 1;  // “读-改-写”复合操作

这个操作分为三步:读、加 1、写。atomic 能保证每一步单独的执行是原子的,但无法保证这三步作为一个整体不被打断。两个线程同时执行这行代码,最终计数结果依然可能少 1。

4.2 跨属性逻辑一致性无法保护

一个 Person 对象有 atomicfirstNamelastName。线程 A 先改 firstName,再改 lastName。线程 B 同时读取这两个属性。线程 B 完全可能读到一个修改后的新名字和一个修改前的旧姓氏,拼出一个不存在的“新人”。业务逻辑的完整性,atomic 无能为力。

因此,开发者如果因为一个属性被标注为 atomic 就产生“这个类已经是线程安全的”这种错觉,反而会埋下更深、更隐蔽的并发漏洞。


5. 实际使用场景分析:真的需要显式使用 atomic 吗?

结论是:在现代 iOS 开发中,几乎找不到一个必须显式使用 atomic 的业务场景。 它的存在基本上属于历史遗留原因和一些极小众的特殊情况。

5.1 历史遗产:MRC 时代的底层安全网

在手动内存管理(MRC)的时代,对象可能在后台线程被 retain / release。如果此时属性的 getter 或 setter 不是原子化的,多线程同时访问同一个属性可能会引发引用计数错乱,最终导致对象过早释放(野指针崩溃)。默认的 atomic 相当于一个底层的安全网,防止属性值本身在被读取时出现“部分写入”的对象指针,从而规避了最极端的崩溃。今天在 ARC 下的通用业务代码中,这个理由已经彻底消失。

5.2 极罕见的单变量“发布”模式

一个很窄的场景:一个后台线程计算完毕后,更新一个简单的状态标识,主线程只负责读取,且不关心该标识与其他属性之间的逻辑一致性。此时 atomic 可以确保主线程读取到的不是状态标识被“写坏”的中间值。

但在实际工程中,遇到这种需求时,我们通常也会用 nonatomic + 专用的锁,或者直接使用 GCD 串行队列来管理状态,行为更加可控。

5.3 你的正确做法:手动模拟原子性

如果你确实需要为某个属性提供原子性的读写,更推荐的做法是:坚持使用 nonatomic,然后用 NSLock@synchronized 等机制,在自定义 getter/setter 中显式加锁

1
2
3
4
5
6
7
8
9
10
- (NSString *)myValue {
@synchronized(self) {
return _myValue;
}
}
- (void)setMyValue:(NSString *)newValue {
@synchronized(self) {
_myValue = [newValue copy];
}
}

这样做的优势是:行为完全由你控制,性能瓶颈一目了然,不会像默认的 atomic 那样在你不注意的时候处处加锁、拖慢整体速度。

5.4 Swift 的视角

值得留意的是,atomic / nonatomic 是 Objective-C 独有的概念。在 Swift 中,所有属性默认行为都等同于 OC 里的 nonatomic,且语言层面根本不提供原子性修饰符。如果你需要原子属性,必须自己用锁实现。苹果自家语言的这种设计,从侧面印证了:靠属性级别的锁来提供线程安全保证,是一条已经被放弃的老路。


6. 最佳实践

  • 永远显式使用 nonatomic。不要依赖默认行为,除非你在维护一段古老的、MRC 下的基础库代码。
  • 忘记“默认 atomic 能防崩溃”的谣言。它只能防极少数的内存撕裂崩溃,却会给你带来强烈的“线程安全”错觉,让你对真正的竞态条件放松警惕。
  • 把线程安全的关注点向上提。属性的线程安全不是问题的终点。使用 GCD 的串行队列、NSOperationQueue、高阶锁等机制,在更高的逻辑层次上保证并发操作的原子性和一致性。
  • 性能考量。UI 相关的所有属性都必须是 nonatomic。在 iOS 的主线程上,每一微秒都值得争取。

7. 结语

atomicnonatomic 的区别,从来都不只是一个“加没加锁”的表面问题。它背后涉及 CPU 指令原子性、内存对齐、Objective-C 历史包袱以及线程安全的设计哲学。作为一名追求严谨的 iOS 工程师,理解这些底层细节,能让你写出更高效、更安全的并发代码,也能帮你厘清那些流传多年的说辞。

现在,你可以在下次代码审查时,清晰而自信地解释:为什么我们要在此处写上 nonatomic,以及为什么那一个悄然保留的默认 atomic 实际上什么也没有保护。