Weak漫谈

Posted by Leonidas's Blog on August 6, 2017

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中,有四个数据结构非常重要,分别是SideTablesSideTableweak_table_tweak_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_t
  • inline_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) 构造方法。

参考文章

iOS 中weak的实现

iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

weak的生命周期:具体实现方法