OC Runtime 中 weak、isa、SideTable 与对象内存结构


OC Runtime 中 weak、isa、SideTable 与对象内存结构
1. 先记住一张大图
Objective-C 对象相关的内存,可以先分成三层:
变量槽位 对象本体 runtime 旁路账本
strong / weak / ivar -> heap object -----------> SideTable / weak_table
比如:
Person *p = [[Person alloc] init];
__weak Person *w = p;
可以粗略理解成:
栈上变量
┌──────────────┐
│ p = 0x1000 │ strong 指针槽位
│ w = 0x1000 │ weak 指针槽位
└──────────────┘
堆上对象
0x1000:
┌────────────────────┐
│ isa │
│ ivar1 │
│ ivar2 │
└────────────────────┘
runtime 旁路表
SideTable:
┌────────────────────────────┐
│ refcnts │ 引用计数补充表
│ weak_table │ weak 反向登记表
└────────────────────────────┘
这里最重要的一句话:
weak 指针本身只是一个普通指针槽位;
weak 的安全性来自 runtime 在旁边维护的 weak_table。
2. iOS 进程里常见的内存区域
从更大的角度看,一个 iOS App 进程里通常有这些内存:
__TEXT 代码段,方法实现、只读常量等
__DATA 全局变量、静态变量、类元数据等
Heap malloc 分配的内存,ObjC 对象通常在这里
Stack 函数调用栈,局部变量、参数、返回地址等
VM / mmap 映射文件、动态库、图片、数据库、缓存文件等
AutoreleasePool 不是独立内存区,而是一套自动释放对象的管理机制
日常讲 isa、引用计数、weak、SideTable,主要是在讲:
Heap 上的 ObjC 对象 + runtime 为对象维护的旁路数据结构
3. 一个 ObjC 对象本体里有什么
最基础的 ObjC 对象结构可以理解成:
struct objc_object {
isa_t isa;
};
如果类有实例变量,实例变量会跟在 isa 后面。
例如:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, weak) id delegate;
@end
对象内存大致是:
Person object
┌────────────────────────────┐
│ isa │
│ _name strong 指针槽位 │
│ _delegate weak 指针槽位 │
└────────────────────────────┘
注意:
strong ivar 和 weak ivar 在对象本体里都是一个指针大小的槽位。
它们真正的区别不在槽位形状,而在编译器调用的 runtime 函数不同。
strong 赋值通常会变成:
objc_storeStrong(&_name, value);
weak 赋值通常会变成:
objc_storeWeak(&_delegate, value);
4. isa 是什么
早期可以把 isa 简单理解为:
对象的 isa 指向类对象
比如:
person object
┌────────────┐
│ isa ───────┼──> Person class
└────────────┘
类对象里有方法、属性、协议、父类、方法缓存等信息:
Class object
┌────────────────────┐
│ superclass │
│ method cache │
│ method list │
│ property list │
│ protocol list │
│ ivar layout │
└────────────────────┘
所以方法调用:
[p sayHello];
大致是:
通过 p 的 isa 找到 Person 类
先查方法缓存
缓存没有再查方法列表
找到 IMP 后调用
5. nonpointer isa 是什么
现代 64 位 iOS runtime 中,isa 通常不是一个单纯的类指针,而是 nonpointer isa。
可以把它理解成:
isa = 类信息 + 对象状态位 + 一小段引用计数
为什么能这么做?
因为 64 位指针并不总是把所有 bit 都用满,而且对象地址、类地址有对齐要求,低位或部分高位可以被 runtime 拿来存状态。
概念上,isa 里可能包含这些信息:
isa_t
┌──────────────────────────┐
│ nonpointer │ 是否是打包 isa
│ has_assoc │ 是否有关联对象
│ has_cxx_dtor │ 是否有 C++ / ARC 析构逻辑
│ shiftcls │ 真正的 Class 信息
│ magic │ runtime 校验位
│ weakly_referenced │ 是否曾经被 weak 指向
│ deallocating │ 是否正在释放
│ has_sidetable_rc │ 引用计数是否溢出到 SideTable
│ extra_rc │ 存在 isa 里的引用计数
└──────────────────────────┘
不同 iOS 版本、不同 CPU 架构,具体 bit 分布会变。面试和理解 runtime 时,重点记概念,不要死记固定 bit 位置。
6. 引用计数放在哪里
现代 runtime 的引用计数大致分两层:
小引用计数:优先放在 isa.extra_rc
放不下时:溢出到 SideTable.refcnts
也就是:
对象本体
┌────────────────────────────┐
│ isa.extra_rc │ 存一部分引用计数
│ isa.has_sidetable_rc │ 是否还有 SideTable 引用计数
└────────────────────────────┘
SideTable
┌────────────────────────────┐
│ refcnts[object] = 额外计数 │
└────────────────────────────┘
为什么不全部放 SideTable?
因为大多数对象引用计数都很小,放在 isa 里更快,不需要额外查表和加锁。
什么时候需要 SideTable?
1. isa.extra_rc 放不下了
2. 对象被 weak 引用,需要 weak_table
3. 某些需要旁路记录的 runtime 状态
7. SideTable 是什么
SideTable 可以理解成 runtime 给对象准备的“旁路账本”。
它不是每个对象一个,而是 runtime 维护一组全局分片表。对象地址会通过哈希映射到某一个 SideTable。
概念结构:
SideTable {
lock;
refcnts;
weak_table;
}
也就是:
SideTable
┌────────────────────────────┐
│ lock │ 多线程保护
│ refcnts │ 引用计数补充账本
│ weak_table │ weak 反向登记表
└────────────────────────────┘
refcnts 记录:
某对象额外的引用计数是多少
weak_table 记录:
某对象被哪些 weak 指针槽位指着
8. weak_table 记录的到底是什么
假设:
Person *p = [[Person alloc] init];
__weak Person *w1 = p;
__weak Person *w2 = p;
很多人容易以为 weak 表是:
w1 -> object
w2 -> object
但 runtime 真正需要的是反向表:
object -> &w1, &w2
也就是:
weak_table
┌──────────────────────────────┐
│ key: object 0x1000 │
│ value: [&w1, &w2] │
└──────────────────────────────┘
为什么记录的是 &w1,不是 w1?
因为对象释放时,runtime 要做的是:
w1 = nil;
w2 = nil;
要修改 w1、w2 的值,就必须知道它们这些变量槽位自己的地址。
所以:
w1 = 对象地址
&w1 = w1 这个变量槽位的地址
对象释放时本质上是:
*&w1 = nil
*&w2 = nil
9. weakly_referenced 什么时候被设置
先看代码:
__weak id w;
这行只是声明了一个 weak 变量槽位,还没有指向具体对象,所以不会让某个对象设置 weakly_referenced。
真正触发的是:
w = obj;
编译器会生成类似:
objc_storeWeak(&w, obj);
runtime 在 weak 存储过程中会做几件事:
1. 找到 obj 对应的 SideTable
2. 加锁
3. 在 weak_table 中登记 object -> &w
4. 设置 obj.isa.weakly_referenced = 1
5. 把 obj 的地址写入 w
6. 解锁
所以结论是:
weakly_referenced 不是 alloc 时设置的;
不是声明 __weak 变量时设置的;
而是在某个非 nil 对象真正被 weak 指向时设置的。
这个 bit 可以理解成:
这个对象曾经被 weak 指向过。
它的作用主要是让对象释放时知道:
我可能需要去 weak_table 清理 weak 指针。
即使后来 weak 被改成 nil 或指向别的对象,这个 bit 也不一定需要改回 0。它偏向于作为释放时的快速判断标志。
10. weak 赋值的完整过程
代码:
__weak id w = obj;
可以理解成:
objc_storeWeak(&w, obj)
流程大致是:
如果 w 原来指向 oldObj:
从 oldObj 的 weak_table 里移除 &w
如果 obj 非 nil:
检查 obj 是否正在 deallocating
如果对象已经在释放,weak 赋值可能得到 nil
在 obj 的 weak_table 记录里加入 &w
设置 obj.isa.weakly_referenced = 1
w = obj
所以 weak 赋值不是普通的:
w = obj
而是:
注销旧登记
登记新 weak 槽位
设置 weakly_referenced
最后写入指针值
11. 对象释放时 weak 怎么被清 nil
代码:
Person *p = [[Person alloc] init];
__weak Person *w1 = p;
__weak Person *w2 = p;
p = nil;
当最后一个 strong 引用消失,流程大概是:
objc_release(obj)
↓
引用计数减到 0
↓
设置 obj.isa.deallocating = 1
↓
进入 dealloc 流程
↓
如果 obj.isa.weakly_referenced == 1
去 SideTable.weak_table 找 obj
找到所有 weak 槽位地址
把这些槽位全部写成 nil
↓
移除 weak_table 中 obj 对应的记录
↓
释放关联对象、strong ivar、C++ 成员等
↓
free 对象内存
weak 清理的核心逻辑可以简化成:
for (location in weak_table[obj]) {
*location = nil;
}
所以:
对象释放前:
w1 = 0x1000
w2 = 0x1000
对象释放后:
w1 = nil
w2 = nil
对象内存被 free
这就是 zeroing weak。
12. deallocating 是什么
deallocating 表示:
对象已经进入释放流程。
这个状态很重要,因为 weak 读取可能发生在多线程环境。
例如:
id strongObj = weakObj;
weak 读取不是简单读指针。runtime 通常会做类似:
读取 weak 槽位
尝试临时 retain 对象
如果对象已经 deallocating,返回 nil
如果 retain 成功,返回一个临时强引用
所以 weak 的安全性不只是“对象死后清 nil”,还包括:
读 weak 时如果发现对象正在死,直接返回 nil。
13. weak 变量所在对象先释放怎么办
还有一种容易忽略的情况:
@interface A : NSObject
@property (nonatomic, weak) id target;
@end
A *a = [[A alloc] init];
NSObject *obj = [[NSObject alloc] init];
a.target = obj;
a = nil;
这里不是 obj 先死,而是 weak 槽位所在的对象 a 先死。
a.target 的 weak 槽位地址曾经被登记在 obj 的 weak_table 里:
weak_table[obj] = [&a->_target]
当 a 释放时,a->_target 这个 weak 槽位也要销毁。
runtime 会做类似:
objc_destroyWeak(&a->_target)
它的作用是:
从 obj 的 weak_table 中移除 &a->_target
所以 weak 有两条清理路径:
1. 被 weak 指向的对象死了:
runtime 找到所有 weak 槽位,把它们置 nil
2. weak 槽位自己死了:
runtime 把这个槽位从目标对象的 weak_table 里注销
14. has_assoc 是什么
关联对象代码:
objc_setAssociatedObject(obj, key, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
一旦对象有关联对象,runtime 会设置:
obj.isa.has_assoc = 1
这样对象释放时,runtime 看到这个 bit,就知道:
不能直接 free;
还要去关联对象表里清理 associated objects。
15. has_cxx_dtor 是什么
has_cxx_dtor 表示这个对象释放时有额外析构逻辑。
可能来自:
C++ 成员变量
ARC strong ivar
ARC weak ivar
其他需要析构的成员
对象释放时,如果有这些内容,runtime 不能直接 free,而是要先执行析构流程:
释放 strong ivar
注销 weak ivar
执行 C++ destructor
然后再释放对象本体
16. weak 和 unsafe_unretained 的区别
__unsafe_unretained id u = obj;
__weak id w = obj;
对象释放后:
u 仍然保存旧对象地址,可能变成野指针
w 会被 runtime 自动置 nil
所以:
[u doSomething]; // 可能崩溃
[w doSomething]; // 安全,等价于 [nil doSomething]
区别本质是:
weak 会进入 weak_table 登记;
unsafe_unretained 不登记,不清零。
17. 最后用口诀记
对象本体:
isa + ivars
isa:
类信息 + 状态位 + 小引用计数
nonpointer isa:
把 Class 指针和对象状态压进一个 isa 里
extra_rc:
存在 isa 里的小引用计数
SideTable:
runtime 旁路账本
refcnts:
引用计数溢出后的补充记录
weak_table:
对象 -> weak 槽位地址列表
weakly_referenced:
第一次有 weak 真正指向对象时设置
deallocating:
对象引用计数归零,进入释放流程时设置
weak 的本质:
不拥有对象,只登记槽位;
对象将死,查表清零。
最短版:
strong 保命
weak 登记
对象将死
查表清零
isa 管身份和状态
SideTable 管放不下的账本
weak_table 管谁 weak 指向我
