0%

iOS weak 的底层实现原理

在 iOS 开发中,weak property 或者是 __weak 修饰的变量,在对象释放后,变量会自动置为 nil。delegate 模式、block 中 weak strong dance 都会用到。

比如,声明一个 weak 的 delegate。

1
@property (nonatomic, weak) id delegate;

weak 用的好可以避免内存泄漏和野指针崩溃。weak 有这么大的作用,底层具体是咋实现的呢?本文探讨下 weak 的底层实现原理。

初探 weak 实现

首先通过汇编来看下,底层怎么实现的。先写几行代码。

1
2
3
4
5
{
id referent = [NSObject new];
__weak id weakObj = referent;
NSLog(@“%@“, weakObj);
}

在 Xcode 中勾选 Debug -> Debug Workflow -> Always Show Disassembly,然后在 NSLog 行加个断点,运行之后可以看到这样一段汇编代码:

disassembly code

这里可以看到 3 个 weak 相关的函数,很容易对应到刚刚的三行代码中。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
id referent = [NSObject new];

// __weak id weakObj = referent;
__weak id weakObj;
objc_initWeak(&weakObj, referent);

// NSLog(@"%@", weakObj);
NSLog(@"%@", objc_loadWeak(&weakObj));

// weakObj 离开作用域,销毁
objc_destroyWeak(&weakObj);
}

知道这三个函数那就好说了,从 github 上可以下载到 runtime 的源码来研究。GitHub - opensource-apple/objc4

底层存储结构

这里不过多的粘贴源码,简单介绍下 weak 底层的存储结构。

weak 底层存储结构

先解释两个单词,referent 是指 weak 变量指向的对象,referrer 是指 weak 变量。

最外层是一个 StripedMap,以 referent 实例地址作为 key,通过哈希,平均映射到 64 个 SideTable 中。
SideTable 中最关键的一个成员是 weak_tableweak_table 的成员 weak_entries 是一个 weak_entry_t 结构体数组,每一个 weak_entry_t 结构体都保存着 referent 地址和指向这个 referent 的所有 weak 变量地址,也就是 referrers 数组。

runtime 源码解释

了解了结构,再来单独聊聊刚刚在汇编中看到的几个函数。

  • objc_initWeak
    通过 referent 去找 SideTable,再遍历 weak_entries 找到对应的 weak entry( weak_entry_t 结构体),将 weak 变量地址添加到对应的 referrers 数组中。当然,如果没有找到 weak entry,会创建一个。

  • objc_loadWeak
    本身 weak 变量已经指向了 referent。objc_loadWeak 内部检查是否是 tag pointer、是否允许 weak reference 等等条件。并查找是否有对应的 weak entry,如果能找到且各种条件满足,则返回 referent 地址,否则返回 nil。

⚠️ 这里返回的 referent 地址,在 runtime 层已经 retain & autorelease。

  • objc_destroyWeak
    这个函数就是 objc_initWeak 的反向操作,把 weak 变量指针从 referrers 数组中移出。如果 referrers 数组为空,那么也顺带会移除 weak entry。

讲了这么多,那这些 weak 指针置为 nil 的逻辑在哪里呢?
从 NSObject 的 dealloc 源码入手,可以看到最后调用到了 weak_clear_no_lock 方法。

  • weak_clear_no_lock
    这个函数主要逻辑是找到对应的 weak entry,遍历 referrers 数组,将所有的 weak 变量都置为 nil,再将 weak entry 移除掉。