dealloc 方法的调用流程
在 Objective-C 中,dealloc 方法的调用流程可以分为两个层面来理解:类继承关系的调用顺序(开发者层面)和 Runtime 内部的执行流程(底层原理层面)。
以下是详细的解析:
1. 类继承关系的调用顺序 (Surface Level)
当一个对象的引用计数变为 0 时,系统会调用 dealloc。在类的继承体系中,调用顺序与 init 方法相反:
- 子类 (Subclass) 的
dealloc首先被调用:先释放子类特有的资源。 - 父类 (Superclass) 的
dealloc随后被调用:子类处理完后,自动向上调用父类的清理逻辑。 - 根类 (NSObject) 的
dealloc最后被调用:完成最终的内存回收。
ARC 与 MRC 的区别:
- MRC (Manual Reference Counting): 必须在
dealloc方法的最后一行显式调用[super dealloc]。 - ARC (Automatic Reference Counting): 严禁显式调用
[super dealloc]。编译器会自动在代码末尾插入调用父类dealloc的代码。
2. Runtime 内部执行流程 (Deep Dive)
这是面试或深入学习中最核心的部分。基于 Apple 开源的 ObjC Runtime 源码 (objc-runtime-new.mm),dealloc 的底层流程如下:
核心路径概览
dealloc -> _objc_rootDealloc -> rootDealloc -> (object_dispose -> objc_destructInstance -> free)
详细步骤分解
第一步:入口
对象调用 dealloc 方法。
第二步:_objc_rootDeallocdealloc 内部直接调用 _objc_rootDealloc(self)。
第三步:rootDealloc (关键判断)
这是核心逻辑所在。Runtime 会判断该对象是否利用了 Non-Pointer ISA(优化的 ISA 指针)以及是否包含特定标志位,来决定走“快速路径”还是“慢速路径”。
Runtime 会检查 isa 中的标志位:
nonpointer: 是否开启了指针优化。weakly_referenced: 是否有弱引用指向该对象。has_assoc: 是否有关联对象 (Associated Objects)。has_cxx_dtor: 是否有 C++ 析构函数(ARC 下用于自动释放 ivar)。has_sidetable_rc: 引用计数是否过大存储在 SideTable 中。
分支 A:快速路径 (Fast Path)
如果对象是“纯净”的(即:没有弱引用、没有关联对象、没有 C++ 析构函数、没有额外的引用计数表),Runtime 会直接调用 C 函数 free(self) 释放内存。流程结束。
分支 B:慢速路径 (Slow Path)
如果上述任一条件不满足(例如对象有关联对象或弱引用),则调用 object_dispose(self)。
第四步:object_disposeobject_dispose 内部做两件事:
- 调用
objc_destructInstance(self)(销毁实例内容)。 - 调用
free(self)(释放内存块)。
第五步:objc_destructInstance (销毁实例的具体操作)
这是清理工作的核心,顺序非常重要:
C++ / ARC Ivar 销毁 (
object_cxxDestruct):- 如果有 C++ 析构函数,调用它。
- 重点: 在 ARC 模式下,编译器会自动生成
.cxx_destruct方法。Runtime 在这一步调用它,自动释放对象持有的所有实例变量 (ivars)。这就是为什么 ARC 下不需要手写_ivar = nil。
移除关联对象 (
_object_remove_assocations):- 如果对象有关联对象(通过
objc_setAssociatedObject添加的),在这里会被移除并释放。 - 注意:关联对象的释放晚于实例变量的释放。
- 如果对象有关联对象(通过
清除弱引用 (
clearDeallocating):- 将指向该对象的所有
weak指针置为nil。 - 处理 SideTable 中的引用计数。
- 将指向该对象的所有
3. 总结图解
obj -> dealloc
|
V
_objc_rootDealloc
|
V
rootDealloc
|
+-- 判断 isa 标志位 (nonpointer, weak, assoc, cxx_dtor, sidetable)
|
+-- [条件满足:纯净对象] ---------> free() -> 结束
|
+-- [条件不满足:复杂对象]
|
V
object_dispose()
|
+-- objc_destructInstance()
| |
| +-- 1. object_cxxDestruct() <-- ARC自动释放成员变量(ivars)
| |
| +-- 2. _object_remove_assocations() <-- 移除关联对象
| |
| +-- 3. clearDeallocating() <-- weak指针置nil
|
+-- free() -> 结束
4. 开发者在 dealloc 中应该做什么?
虽然 ARC 帮我们处理了实例变量的释放,但以下工作仍需在 dealloc 中手动处理:
- 移除观察者:
- 移除
KVO观察者(虽然 iOS 11+ 很多情况不需要了,但为了安全通常还是建议移除)。 - 移除
NSNotificationCenter监听(iOS 9+ 之后大部分情况不需要手动移除,但 block 形式的监听除外)。
- 移除
- 停止定时器:
- 如果持有
NSTimer或CADisplayLink,必须invalidate,否则可能导致内存泄漏(循环引用)。
- 如果持有
- 清理非 OC 对象:
- 释放
CoreFoundation对象(CFRelease)。 - 关闭文件句柄、Socket 连接等 C 语言层面的资源。
- 释放
- 清理代理:
- 如果 delegate 是
assign修饰的(极少见,通常是 weak),需要置为 nil,防止野指针。
- 如果 delegate 是
总结关键点 (面试金句)
- 调用顺序:子类 -> 父类。
- ARC 释放成员变量的时机:在
.cxx_destruct阶段,由 Runtime 自动调用,早于关联对象的释放和 weak 指针的清空。 - Weak 指针置 nil 的原理:在
clearDeallocating阶段,通过 SideTable 查找所有指向该对象的 weak 指针并置空。