浅谈weak与unowned
在iOS的开发中,经常会有A持有B,但是B又持有A的问题,这就是老生常谈的循环引用,目前最常用的方法就是使用weak或者unowned去打破循环。接下来浅谈下两者的底层实现原理以及两者的对比。
weak
weak的底层原理分为Objective-C与swift的两种不同的机制。两者的核心差异是中心化与去中心化。
Objective-C
在Objective-C中维护了一张全局的weak哈希表,所有的weak指针都会存储在这里,此处存储的key是对象的地址,Value是weak指针的地址(weak指针就是用的地方的地址,比如weak var a = temp() 那么weak指针就是a的地址),value根据weak指针的数量调整value是一个数组还是一个哈希表。当对象死亡时,会对大哈希表进行查找,然后去找到key对应的weak指针进行置空。
OC的weak销毁相对来说会比较暴力,下方为一个销毁的例子。
// 1. 创建对象 (假定 obj 指向 0xA00)
NSObject *obj = [[NSObject alloc] init];
// 2. 声明 weak 指针 (假定 p 变量本身的地址是 0xB00)
// 此时 Runtime 开始介入
__weak NSObject *p = obj;
1.当 obj 的引用计数为 0 则准备销毁。
2.deallc开始调用Runtime的清除函数。
3.Runtime会拿着obj的地址0xA00去weak表去查找
4.找到之后取出Value:[0xB00,0xC00,0xD00 ... ]
5.核心操作:Runtime遍历这个名单,通过地址找到变量p 0xB00
强行将0xB00内存里的数据写成0 (nil)
6.销毁weak表中的这条记录
swift
swift采用了一种更加高效的方式,叫做 Side table (散列表/辅助表) 结合 惰性置空 (Lazy Zroing) 每一个对象都会拥有类似OC中的weak表,weak指针指向的是这个weak表不是对象本身,如果是强引用则指向的是对象地址。
struct HeapObject { // 这个是对象的头部
Metadata *isa;
// 64位仅仅是一个数字,存着 Strong 和 Unowned 计数
// 当有weak指向它,它就会变化为一个指针,指向在堆上额外开辟的Side Table。
uint64_t refCounts;
}
class SideTable {
HeapObject *object; // 1. 指回原对象的指针
Atomic<StrongRefCount> strong; // 2. 强引用计数
Atomic<UnownedRefCount> unowned; // 3. 无主引用计数
Atomic<WeakRefCount> weak; // 4. 弱引用计数 (关键!)
}
这张图可以作为理解的参考。
在学习过程中,又产生个疑问,避免后续忘记现在记录下来,就是当既有weak指向A又有strong指向A,那么strong是怎样工作的?答案是:strong指向A的会直接读取A,发现有side table表就会进行读取指针找到这个表,然后在表上strong计数加一,同理strong消失也会找到此处进行减一。
惰性置空机制:swift并不像OC那样统一去抹除weak指针,而是在你去访问side table表的时候才会返回nil,并且将weak数减一。这个side table表在对象被销毁的时候,会保留直至weak数等于0才会被释放掉。
unowned
这个就以swift的为主,毕竟这个的使用是非常的少,首先说下对象的三段式生命周期,swift并不是对象一死就消失。
| 阶段 | 条件 | 状态描述 | 内存情况 |
|---|---|---|---|
| 1. Live (存活) | Strong > 0 | 对象正常工作。 | 完整内存。 |
| 2. Deinited (僵尸) | Strong = 0 Unowned > 0 |
deinit 已执行,属性已销毁。但对象头部(HeapObject)还在。 | 属性内存释放,头部内存保留。 |
| 3. Dead (死亡) | Strong = 0 Unowned = 0 |
对象彻底消失。 | 头部内存被 free。 |
A. 赋值阶段 (unowned var p = obj)
当在这个引用被赋值时:
-
Runtime 不会增加 Strong Count。
-
Runtime 会增加 Unowned Count (+1)。
-
后果:只要 p 还在,obj 就算死(Strong=0),也不能死透(进入 Dead 阶段),它必须卡在 Deinited 阶段,保留头部给 p 做检查。
B. 访问阶段 (print(p.name))
当你访问一个 unowned 变量时,编译器会插入检查代码(swift_unownedLoadStrong):
-
直接寻址:拿着指针直接找到内存中的对象头部(此时内存肯定没被操作系统回收,因为 Unowned Count > 0)。
-
原子检查:读取头部引用计数的状态位。
-
分支判断:
-
如果对象是 Live:原子操作让 Strong + 1,正常返回对象引用。
-
如果对象是 Deinited:说明对象逻辑已死(属性都没了),此时你还来访问,触发 swift_abortRetainUnowned,导致 App 崩溃。
-
C. 销毁阶段
当持有 unowned 引用的变量 p 离开作用域或被销毁时:
-
它会减少对象的 Unowned Count (-1)。
-
如果此时 Strong == 0 且 Unowned == 0,对象才会真正调用 free() 释放头部的物理内存。
swift中的unowned是相对来说是安全的,仅仅会触发crash并不会变成野指针去访问脏数据
总结
无论是weak还是unowned,都是为了解决循环引用这个问题,他们的解决方式都是,strong的引用记数不增加,而是一个新的代表这个的若引用无主引用的计数,去打破强持有,从而去解决这个有可能产生的循环引用问题。
整体上来说weak更加安全,就算访问的对象已经销毁也不会导致崩溃,而unowned最好的情况就是崩溃,最坏的情况访问到脏数据,导致展示数据页面等等的错误,但是unowned的速度以极小的优势超过了weak,还是推荐使用weak,非必要不使用unowned。