weak 表的清理发生在一个对象的 生命终点,确切地说,是在 dealloc 流程执行到 objc_destructInstance 这一步时。
清理过程并不是一蹴而就的,而是一场严密的“定点清除”行动。
1. 清理的触发时机:dealloc 流程
当对象的引用计数归零,系统调用 dealloc。为了保证效率,Runtime 并没有在第一时间清理 weak 表,而是先进行一系列状态检查。
清理的完整调用链如下:
_objc_rootDeallocrootDeallocobjc_destructInstanceclearDeallocating(核心清理函数)
2. 核心函数:clearDeallocating
这是 weak 指针变 nil 的真正时刻。在 clearDeallocating 内部,Runtime 会做两件事:
- 检查
isa标志位:首先读取isa指针。如果weakly_referenced位为0,说明没有任何人弱引用过这个对象,直接跳过清理,极大提升销毁速度。 - 执行
weak_clear_no_lock:如果标志位为1,则进入SideTable锁定weak_table,开始地毯式搜索。
3. 清理的三个物理步骤
当进入 weak_clear_no_lock 后,清理工作分为以下三步:
- 定位 Entry:通过被销毁对象的 地址 作为 Key,在全局的
weak_table中找到对应的weak_entry_t(也就是存储该对象所有弱引用指针的数组)。 - 强制置空 (Zeroing) :遍历该 Entry 中的所有弱引用指针地址。执行类似
*ptr = nil的操作。这就是为什么你的变量会在那一瞬间突然变成nil。 - 从表中抹除:一旦所有指针都已置空,该 Entry 会从
weak_table哈希表中移除,腾出内存供其他对象使用。
4. 为什么不能提前清理?
你可能会想,为什么不在引用计数归零的那一刻就清理?
- 竞态条件 (Race Condition) :在
dealloc的早期阶段,对象可能还在执行自定义的销毁逻辑。如果提前清理了weak表,那么在该对象的dealloc方法里,如果尝试使用weakSelf就会拿到nil,导致逻辑错误。 - 性能考量:
weak表的清理涉及全局SideTable的加锁。将其放在销毁流程的最后一步,可以确保对象已经处于“准死亡”状态,减少锁持有的时间。
5. 常见误区:对象死了,weak 指针就一定变 nil 吗?
绝大多数情况是,但不绝对。
有一种特殊情况:如果对象所在的内存被直接 free() 掉,而没有走 Objective-C 的 dealloc 流程(例如你在底层用 C 的方式操作内存),那么 weak 表里对应的记录永远不会被触发清理。这会导致 weak 指针变成一个野指针,再次访问就会 Crash。所以,永远不要绕过 Runtime 手动管理 Objective-C 对象的生命周期。
总结
weak 表是在对象 物理销毁的前一刻(即 dealloc 内部)被清理的。它的清理标志着对象作为“引用实体”在内存中的彻底消失。