weak漫谈
被weak修饰的对象在被释放的时候会发生什么?
会被置为nil.
weak怎么做到释放后将对象变为nil
Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。
weak表其实是一个hash(哈希)表,Key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象的地址)数组。
1、初始化时:runtime会调用objc_initWeak
函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak
函数会调用 objc_storeWeak()
函数, objc_storeWeak()
的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
weak实现细节
在runtime中,有四个数据结构非常重要,分别是SideTables
,SideTable
,weak_table_t
和weak_entry_t
。它们和对象的引用计数,以及weak引用相关。
先说一下这四个数据结构的关系。 在runtime内存空间中,SideTables
是一个64
个元素长度的hash数组,里面存储了SideTable
。SideTables的hash键值就是一个对象obj的address。
因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量只有64个,所以会有很多obj共用同一个SideTable。
他们的结构如下:
SideTables
如图,SideTables是一个全局的hash数组,里面存储了64个SideTable,其长度为64。
SideTabls可以通过全局的静态函数获取:
1
2
3
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
SideTabls
实质类型为模板类型StripedMap
.StripedMap
是一个以void *
为hash key, T
为vaule的hash 表。
SideTable
SideTable主要存放了OC对象的引用计数和弱引用相关信息
SideTable 这个结构体主要用于管理对象的引用计数和 weak 表。在 NSObject.mm
中有其数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct SideTable {
spinlock_t slock;//保证原子操作的自旋锁
RefcountMap refcnts;//引用计数的 hash 表
weak_table_t weak_table;//weak 引用全局 hash 表
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// Address-ordered lock discipline for a pair of side tables.
template<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
其中这3个结构很关键:
1
2
3
spinlock_t slock;
RefcountMap refcnts;//引用计数的 hash 表
weak_table_t weak_table;//weak 引用全局 hash 表
-
refcents
是一个hash map,存储了每个对象的引用信息,其key是obj的地址,value是obj对象的引用计数。 weak_table
也是一个hash map,存储了弱引用obj的指针的地址,其key为obj地址,value是弱引用obj的指针的地址数组。hash表的节点类型是weak_entry_t
。slock
自旋锁,用于上锁/解锁 SideTable。
再看看SideTable的构造和析构函数:
1
2
3
4
5
6
7
8
9
// 构造函数
SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
~SideTable() {
_objc_fatal("Do not delete SideTable.");
}
通过析构函数~SideTable()
的函数体可以看出苹果设计的SideTable其实不希望被析构,不然会引起fatal 错误。
下面看看这3个关键结构是做什么的。
1 spinlock_t slock
spinlock_t
是一个uint32_t
类型的非公平的自旋锁
。所谓非公平,就是说获得锁的顺序和申请锁的顺序无关,也就是说,第一个申请锁的线程有可能会是最后一个获得到该锁,或者是刚获得锁的线程会再次立刻获得到该锁,造成饥饿等待。 同时,在OC中,_os_unfair_lock_opaque
也记录了获取它的线程信息,只有获得该锁的线程才能够解开这把锁。另外,它也是对已废弃的OSSpinLock
的一种替代。
1
2
3
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
2 RefcountMap refcnts
RefcountMap refcnts
用来存储OC对象的引用计数。它实质上是一个以objc_object
为key的hash表,其vaule就是OC对象的引用计数。同时,当OC对象的引用计数变为0时,会自动将相关的信息从hash表中剔除。RefcountMap的定义如下:
1
2
3
// RefcountMap disguises its pointers because we
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;
实质上是模板类型objc::DenseMap
。模板的三个类型参数DisguisedPtr<objc_object>
,size_t
, true
分别表示DenseMap的hash key类型,value类型,是否需要在value==0的时候自动释放掉响应的hash节点,这里是true。
3 weak_table_t weak_table
weak_table_t weak_table
用来存储OC对象弱引用的相关信息。我们知道,SideTables一共只有64
个节点,而在我们的APP中,一般都会不只有64个对象,因此,多个对象一定会重用同一个SideTable节点,也就是说,一个weak_table会存储多个对象的弱引用信息。因此在一个SideTable中,又会通过weak_table作为hash表再次分散存储每一个对象的弱引用信息。
weak_table_t的定义如下:
1
2
3
4
5
6
7
8
9
10
11
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
- weak_entries : hash动态数组,用来存储弱引用对象的相关信息
weak_entry_t
- num_entries : hash数组中的元素个数
- mask : hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
- max_hash_displacement : 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值
4 weak_entry_t
weak_table_t
中存储的元素是weak_entry_t
类型,每个weak_entry_t
类型对应了一个OC对象的弱引用信息。
weak_entry_t
的结构和weak_table_t
很像,同样也是一个hash表,其存储的元素是weak_referrer_t
,实质上是弱引用该对象的指针的指针,即 objc_object **new_referrer
, 通过操作指针的指针,就可以使得weak 引用的指针在对象析构后,指向nil。
1
2
3
4
5
// The address of a __weak variable.
// These pointers are stored disguised so memory analysis tools
// don't see lots of interior pointers from the weak table into objects.
typedef DisguisedPtr<objc_object *> weak_referrer_t;
weak_entry_t 的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line_ness != REFERRERS_OUT_OF_LINE then the set
* is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
// out_of_line_ness field overlaps with the low two bits of inline_referrers[1].
// inline_referrers[1] is a DisguisedPtr of a pointer-aligned address.
// The low two bits of a pointer-aligned DisguisedPtr will always be 0b00
// (disguised nil or 0x80..00) or 0b11 (any other address).
// Therefore out_of_line_ness == 0b10 is used to mark the out-of-line state.
#define REFERRERS_OUT_OF_LINE 2
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象
// 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
union {
struct {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}
weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}
weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent) // 构造方法,里面初始化了静态数组
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};
DisguisedPtr<objc_object> referent
:弱引用对象指针摘要。其实可以理解为弱引用对象的指针,只不过这里使用了摘要的形式存储。(所谓摘要,其实是把地址取负)。union
:接下来是一个联合,union有两种形式:定长数组weak_referrer_tinline_referrers[WEAK_INLINE_COUNT]
和 动态数组weak_referrer_t *referrers
。这两个数组是用来存储弱引用该对象的指针的指针的,同样也使用了指针摘要的形式存储。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT
时,使用定长数组。当超过WEAK_INLINE_COUNT
时,会将定长数组中的元素转移到动态数组中,并之后都是用动态数组存储。关于定长数组/动态数组 切换这部分,我们在稍后详细分析。bool out_of_line()
该方法用来判断当前的weak_entry_t
是使用的定长数组还是动态数组。当返回true,此时使用的动态数组,当返回false,使用静态数组。weak_entry_t& operator=(const weak_entry_t& other)
:赋值方法weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
构造方法。