iOS 内存深度解析:ARC 到底是怎么管理对象生命周期的
很多人对 ARC 的理解停留在三句话:
"ARC 会自动 retain / release;引用计数归零就释放;有循环引用就内存泄漏。"
这三句没错,但只是用户层心智模型。
真正要搞懂 iOS 内存,得往下看三层:
- 编译期:Clang 在什么地方插 retain / release
- 运行期:objc_retain / objc_release 怎么改引用计数
- 底层存储:isa、extra_rc、SideTable、weak table 如何协作
一、先纠偏:ARC 不是 GC,也不是"自动内存管理黑盒"
ARC(Automatic Reference Counting)本质是:
引用计数机制不变,retain / release 由编译器和 runtime 替你插。
它和 Java / Go 的垃圾回收(GC)完全不同:
| 维度 | ARC | GC |
|---|---|---|
| 释放时机 | 引用计数归 0 立即释放 | 后台扫描,延迟回收 |
| 是否停顿 | 无 STW | 可能有 stop-the-world |
| 循环引用 | 不能自动解决 | 可达性分析可回收 |
| 成本位置 | 每次引用变更都有小成本 | 周期性大成本 |
所以 ARC 的核心优势是:确定性销毁;
核心代价是:循环引用要人来处理。
二、对象生命周期的全景图
一个 Objective-C 对象的一生大概是:
objectivec
alloc/init
↓
被强引用持有(retain / storeStrong)
↓
被传递、被赋值、被返回
↓
强引用逐个消失(release / 变量出作用域)
↓
strong count = 0
↓
调用 dealloc
↓
清理 weak 引用、关联对象、C++ 析构
↓
内存归还给堆分配器(malloc / free)
ARC 做的事,就是在"被持有"和"不再被持有"的边界上,自动插入正确的引用计数操作。
三、编译期:Clang 到底插了什么
你写:
ini
{
id obj = [NSObject new];
id other = obj;
}
Clang 在 IR / 机器码层面大致会变成:
less
id obj = objc_msgSend(NSObject.class, @selector(new));
// new 已经返回 +1 对象
objc_storeStrong(&other, obj);
// other = obj:先 retain 新值,再 release 旧值
objc_storeStrong(&obj, nil); // 作用域结束
objc_storeStrong(&other, nil); // 作用域结束
关键运行时函数不是 [obj retain],而是:
objc_retainobjc_releaseobjc_storeStrongobjc_retainAutoreleasedReturnValueobjc_autoreleaseReturnValueobjc_initWeak/objc_destroyWeak
ARC 不是"发消息",而是"调 runtime C 函数"。
原因:更快,也更容易被优化器识别。
1. 不同修饰符,插不同的代码
| 修饰符 | 行为 |
|---|---|
__strong(默认) |
赋值 retain,离开作用域 release |
__weak |
不 retain;对象死时自动置 nil |
__unsafe_unretained |
不 retain;对象死了变成野指针 |
__autoreleasing |
隐式进 autorelease pool |
例如:
ini
__weak id w = obj;
编译器会生成:
scss
objc_initWeak(&w, obj);
// 作用域结束:
objc_destroyWeak(&w);
四、运行期:retain / release 改的到底是什么
1. Tagged Pointer:根本不进堆
像小 NSNumber、短 NSString:
python
@1
@"abc"
可能是 Tagged Pointer ------值直接编码在指针里,没有堆对象,没有引用计数,objc_retain 直接返回。
kotlin
if (obj->isTaggedPointer()) return obj;
所以:
Tagged Pointer 不受 ARC 管理,也不是"对象生命周期"问题。
2. Non-pointer isa:引用计数藏在指针里
64 位时代,isa 不只是"指向 Class 的指针",而是位域:
arduino
isa_t:
nonpointer 1
has_assoc 1
has_cxx_dtor 1
shiftcls 33 // 真正的类指针
magic 6
weakly_referenced 1
deallocating 1
has_sidetable_rc 1
extra_rc 19 // 引用计数 - 1
也就是说:
- 普通对象:引用计数直接放在 isa 的 extra_rc 里
extra_rc能存:2^19 - 1 = 524287个强引用- 大多数 retain / release 只是一次原子 CAS 改 isa
arduino
// retain 简化逻辑
if (isa.extra_rc < MAX) {
isa.extra_rc += 1; // 原子操作
} else {
// 溢出,搬一半到 SideTable
}
这就是为什么 ARC 在 iOS 上够快:
99% 的 retain/release 不改全局表、不拿大锁。
3. SideTable:isa 装不下时的兜底仓库
当发生以下情况:
- 引用计数超过 extra_rc 上限
- 对象被
__weak引用过 - 有关联对象 / 需要弱引用表
runtime 会用 SideTable。
全局有 64 张 SideTable,按对象地址哈希分桶,降低锁竞争:
arduino
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; // 对象地址 -> 额外引用计数
weak_table_t weak_table; // 对象地址 -> 弱引用列表
};
所以"引用计数存在哪里"的标准答案是:
先存 isa.extra_rc,溢出或需要弱引用时,存 SideTable。
五、release 到 0 之后发生了什么
这是很多人没讲透的一步。
scss
objc_release(obj)
流程大概是:
-
extra_rc -= 1 -
如果还有余量 → 结束
-
如果 extra_rc 不够 → 从 SideTable 借/减
-
如果总强引用数 = 0:
- 标记
deallocating = 1 - 调用
dealloc
- 标记
-
dealloc里做:- 调用 C++ 析构
- 清理关联对象
- 清理 weak 引用
- 调用
object_dispose
-
内存归还分配器
1. weak 引用为什么不会野指针
对象 dealloc 时,runtime 会:
objectivec
查 SideTable.weak_table
→ 找到所有指向该对象的 weak 指针
→ 全部置 nil
→ 再释放对象
所以:
ini
__weak id w = obj;
// obj 释放后
w == nil; // 安全
而:
ini
__unsafe_unretained id u = obj;
// obj 释放后
u; // 野指针,访问即 crash
六、autorelease:不是"晚一点释放",而是"交给池子托管"
1. MRC 时代
ini
- (NSString *)name {
NSString *s = [[NSString alloc] initWithFormat:@"%@"];
return [s autorelease];
}
对象先进入当前 autorelease pool,runloop 一轮结束后才 release。
2. ARC 时代
你写:
objectivec
- (NSString *)name {
return [NSString stringWithFormat:@"hi"];
}
编译器生成:
kotlin
return objc_autoreleaseReturnValue(retval);
调用方:
ini
id x = objc_retainAutoreleasedReturnValue([obj name]);
这里有一个关键优化:
如果调用方马上 retain,就不进 autorelease pool,直接"移交所有权"。
这是 ARC 性能比"傻瓜式 autorelease"好的核心原因。
3. @autoreleasepool 什么时候真有用?
主线程 RunLoop 每一轮都有池子。
但下面场景要自己加:
ini
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
NSString *s = [NSString stringWithFormat:@"%d", i];
// 临时对象本轮就释放
}
}
否则几十万临时对象撑到 RunLoop 结束,峰值内存爆炸。
七、属性、变量、Block:生命周期最容易翻车的地方
1. 属性默认是 strong
objectivec
@property (nonatomic) UIView *view;
等于:
objectivec
__strong UIView *view;
2. Block 捕获 self 是强引用
ini
self.block = ^{
[self doSomething];
};
形成:
lua
self → block → self
解决:
ini
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf doSomething];
};
这里 strongSelf 的作用是:
在 block 执行期间"临时保活",避免执行到一半 self 没了。
八、Swift 的 ARC 和 ObjC 有什么区别
Swift 也是 ARC,但更彻底:
- 没有
retain/release/autorelease - 没有
__weak指针类型,只有weak var - 有
unowned - 编译器对值类型/引用类型划分更清晰
Swift 对象头大概:
csharp
isa
strong ref count
unowned ref count
weak ref count / flags
但思想一样:
strong count = 0 → deinit → 清 weak → 释放
九、一张图总结 ARC 的生命周期决策
objectivec
新对象创建
↓
有几个 __strong 指向它?
├─ >0 → 活着
└─ =0 → dealloc
↓
weak 引用置 nil
关联对象清理
C++ / Swift 资源清理
↓
内存回收
ARC 的真相只有一句:
它不决定"对象该不该活",它只是严格记账:谁强引用你,你就多活一会儿;没人强引用你,你就立刻死。
十、面试/工程里最该记住的 10 句话
- ARC 不是 GC,是"编译期插 retain/release + runtime 管计数"。
- 引用计数先放
isa.extra_rc,不够才