__weak修饰符的底层实现分析

关于__weak修饰符的作用可能大家都知道,附有__weak修饰符的变量对对象是弱引用,对象的引用计数不会增加,在使用之前,附有__weak修饰符的变量指向的对象会被放到 autoreleasepool 中,等该对象被释放时会同时将其置为nil。但是你知道objc是怎样管理weak对象的吗?《Objective-C高级编程》1.4节对__weak的实现做了大致的讲解,再结合源码我们可以进一步理解__weak的实现过程。

从苹果的开源网站下载objc4的源码:https://opensource.apple.com/tarballs/objc4/,我下载的是objc4-750这个版本。之后可以在工程中找到objc-weak.h和objc-weak.mm,从objc-weak.h中可以看到跟weak相关的类和函数,从objc-weak.mm中可以看到对weak对象的增加、删除等操作。weak对象的初始化和销毁等入口函数在NSObject.mm中可以找到。

__weak变量的存储

1
id __weak obj1 = obj;

该源代码可转换成如下形式:

1
2
3
4
/* 编译器模拟代码 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destroyWeak(&obj1);

在源码中定位到objc_initWeak函数,顺着它的实现过程,我们可以发现与weak对象的存储相关的类有这样几个:

SideTable

SideTable的大致定义如下:

1
2
3
4
5
6
7
8
9
struct SideTable {
// 自旋锁,用于对SideTable操作时进行加锁
spinlock_t slock;
// 存储对象的引用计数
RefcountMap refcnts;
// 存储对象的弱引用
weak_table_t weak_table;
...
}
  • 全局有一个SideTables,通过对象的地址可以从中找到与其对应的SideTable,进而拿到该对象的refcntsweak_table
  • SideTables的定义可以看到,实际上它是一个StripedMap<SideTable>
1
2
3
static StripedMap<SideTable>& SideTables() {
return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

StripedMap是一个通用的散列表,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename T>
class StripedMap {
// 定义散列表的大小
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif

struct PaddedT {
T value alignas(CacheLineSize);
};

PaddedT array[StripeCount];

// 散列函数:通过指针地址计算出index
static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}
...
}

在发生散列冲突的时候,多个对象会共用一个SideTable,也就是多个对象会共用一个refcntsweak_table

weak_table_t

1
2
3
4
5
6
7
8
9
10
struct weak_table_t {
// 存储weak_entry_t
weak_entry_t *weak_entries;
// 当前weak_entry_t的个数
size_t num_entries;
// 当前数组能够容纳的最大个数
uintptr_t mask;
// 发生散列冲突时,能够遍历的元素的最大个数
uintptr_t max_hash_displacement;
};

weak_table_t也是一个散列表,存放了一组weak_entry_tweak_entry_t是用来存放被弱引用的对象(referent)和对该对象的所有弱引用(referrers)。所以weak_table_t存了多个对象的弱引用对应关系。下面来看看作为散列表,它的散列函数是如何实现的,它是如何来处理散列冲突的,以及如何来进行扩容/缩容的。

weak_entry_for_referent,根据对象地址查找对应的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
static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
// 对象不能为空
assert(referent);

weak_entry_t *weak_entries = weak_table->weak_entries;

if (!weak_entries) return nil;

// 计算索引
size_t begin = hash_pointer(referent) & weak_table->mask;
size_t index = begin;
size_t hash_displacement = 0;
// 如果begin位置不是传入的referent,则从begin开始向下,再向上遍历数组
while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_table->weak_entries);
hash_displacement++;
// 如果查询元素的个数已经超过了预设值,则直接返回nil,防止死循环一直找不到
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
}

return &weak_table->weak_entries[index];
}
  • 散列函数:hash_pointer(referent) & weak_table->mask;
  • 通过开放寻址的方式解决散列冲突的问题。

weak_grow_maybeweak_compact_maybe,对weak_table_t进行扩容和缩容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table)
{

size_t old_size = TABLE_SIZE(weak_table);

// Grow if at least 3/4 full.
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}

// Shrink the table if it is mostly empty.
static void weak_compact_maybe(weak_table_t *weak_table)
{

size_t old_size = TABLE_SIZE(weak_table);

// Shrink if larger than 1024 buckets and at most 1/16 full.
if (old_size >= 1024 && old_size / 16 >= weak_table->num_entries) {
weak_resize(weak_table, old_size / 8);
// leaves new table no more than 1/2 full
}
}
  • 如果数组中元素个数超过了3/4,进行扩容,扩大到当前数组大小的2倍。
  • 如果数组的长度大于1024了,并且实际元素的个数还不足其1/16,进行缩容,缩减到当前数组长度的1/8。
  • weak_resize函数会一次性将weak_entry_t搬到new_entries中,最后释放old_entries

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
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
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;
}
}
};

weak_entry_t依然是一个散列表,但是在数据的存储上稍有不同,在out_of_line为false时,使用inline_referrers存储弱引用,这是一个固定大小为4的数组。当out_of_line为true时,会使用referrers存储弱引用。可以看到使用referrers时的结构和weak_table_t非常相似。下面看一下添加一个弱引用的过程。

append_referrer

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
static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
// out_of_line为false时
if (! entry->out_of_line()) {
// 直接从0开始遍历,看是否有能插入的位置
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}

// 插入到inline中,初始化outline
weak_referrer_t *new_referrers = (weak_referrer_t *)
calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
// 将inline中的对象搬到outline中
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
new_referrers[i] = entry->inline_referrers[i];
}
entry->referrers = new_referrers;
entry->num_refs = WEAK_INLINE_COUNT;
entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
entry->mask = WEAK_INLINE_COUNT-1;
entry->max_hash_displacement = 0;
}

// 确保此时out_of_line为true
assert(entry->out_of_line());

// 如果个数超过数组长度的3/4,进行扩容
if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
return grow_refs_and_insert(entry, new_referrer);
}
size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
size_t index = begin;
size_t hash_displacement = 0;
// 发生散列冲突,使用开放寻址找到合适的插入位置
while (entry->referrers[index] != nil) {
hash_displacement++;
index = (index+1) & entry->mask;
if (index == begin) bad_weak_table(entry);
}
// 更新max_hash_displacement
if (hash_displacement > entry->max_hash_displacement) {
entry->max_hash_displacement = hash_displacement;
}
weak_referrer_t &ref = entry->referrers[index];
ref = new_referrer;
entry->num_refs++;
}
  • 判断是否可以将对象放入inline,如果能则插入返回。如果不能,此时out_of_line还是false,说明outline还没开始用,则初始化outline。
  • 判断outline是否需要扩容,如果需要则调用grow_refs_and_insert进行扩容。
  • 计算插入位置:w_hash_pointer(new_referrer) & (entry->mask);
  • 如果发生了散列冲突,则用开发寻址方式找到新的插入位置。如果寻找的此时超过了之前的max_hash_displacement,则更新它到最新值。

__weak变量的初始化与销毁

objc_initWeakobjc_destroyWeak__weak变量初始化和销毁的入口函数,源代码中对其注释还是挺详细的。其中两个函数的注释中都写了This function IS NOT thread-safe with respect to concurrent modifications to the weak variable,要额外注意,在多线程并发访问时不要修改__weak变量,其底层实现是非线程安全的。这两个函数都调用了storeWeak这个函数,通过传参控制是添加还是移除弱引用。

storeWeak

1
2
3
4
5
6
// storeWeak用的模板参数
enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
enum CrashIfDeallocating {
DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};

在初始化时haveOld=false,haveNew=true。在销毁时haveOld=true,haveNew=false。

这个函数做的事大致如下:

  • 从全局的SideTables,通过对象地址取出对应的SideTable
  • 如果haveOld为true,调用weak_unregister_no_lock函数,移除该弱引用。
  • 如果haveNew为true,调用weak_register_no_lock函数,添加该弱引用。然后设置对应的isa.weakly_referenced为true,这样在销毁对象时,如果该标志位为true,会调用weak_clear_no_lock函数将对象置为nil。

weak_unregister_no_lock

这个函数做的事大致如下:

  • 判断对象是否正在销毁中,如果是,则crash或返回nil。
  • 调用weak_entry_for_referent函数获取weak_entry_t,如果能拿到对象,则调用append_referrer函数,插入该弱引用。
  • 如果没能拿到weak_entry_t,则新创建一个。再调用weak_grow_maybe函数如果需要扩容时进行扩容。然后调用weak_entry_insertweak_table中添加一个新的entry。

可以看到,这个函数其实就是对上一节说的一些方法的综合使用。

weak_unregister_no_lock

这个函数做的事大致如下:

  • 通过weak_entry_for_referent函数获取entry,如果存在则调用remove_referrer函数,将该弱引用从entry中移除。
  • 判断该entry的outline和inline是否都为空,如果是的话,则调用weak_entry_remove函数,将该entry从weak_table中移除。
  • 注意,该函数并没有将对象置为nil。

weak_clear_no_lock

执行dealloc时,如果对象的isa.weakly_referenced为true,会调用该函数将指向该对象的所有弱引用置为nil。

这个函数做的事大致如下:

  • 通过weak_entry_for_referent函数获取entry,如果为nil直接返回。
  • 从entry的outline或inline中去出referrers,遍历该referrers依次将引用设置为nil。
  • weak_table中移除该entry。

__weak变量与autoreleasepool

__weak变量在使用之前会被注册到 autoreleasepool 中。

1
2
id __weak obj1 = obj;
NSLog(@"%@", obj1);

该源代码可转换成如下形式:

1
2
3
4
5
6
7
/* 编译器模拟代码 */
id obj1;
obj_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

在NSObject.mm中可以找到objc_loadWeak函数,从注释中可以很清楚地看到,这样做是为了在使用它时能确保__weak变量引用的对象一直都在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** 
* This loads the object referenced by a weak pointer and returns it, after
* retaining and autoreleasing the object to ensure that it stays alive
* long enough for the caller to use it. This function would be used
* anywhere a __weak variable is used in an expression.
*
* @param location The weak pointer address
*
* @return The object pointed to by \e location, or \c nil if \e location is \c nil.
*/
id
objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}

objc_loadWeakRetained函数取出__weak变量引用的对象并retain,然后objc_autorelease再将对象注册到 autoreleasepool 中。

小结

从上面的分析中可以总结几点平时在使用__weak时的注意点:

  • 如果大量使用附有__weak修饰符的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak修饰符。
  • 如果大量使用附有__weak修饰符的变量,注册到 autoreleasepool 的对象也会大量增加,因此使用附有__weak修饰符的变量时,最好先暂时赋值给附有__strong修饰符的变量后再使用。
1
2
3
id weak o = obj;
id tmp = o;
NSLog(@"%@", tmp);
  • __weak不是线程安全的,在多线程中使用时要额外注意,否则可能一个线程已经释放了对象所占的内存空间,但是对其弱引用还没有释放,此时另一个线程用弱引用去访问该对象,就会发生内存访问错误的crash。
  • __weak的底层实现多次使用了散列表,对散列表使用的奇巧淫技我们也可以借鉴到平时的开发中去。

参考资料

【1】 《Objective-C高级编程》1.4节

【2】 理解 ARC 实现原理

本文作者:意林
本文链接:http://shinancao.cn/2019/07/15/iOS-weak/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!