__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 用来存放引用计数的表(refcnts)和弱引用的表(weak_table)。
1
2
3
4
5
6
7
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

...
}
  • weak_table_t 全局弱引用表,存储一组weak_entry_t对象。
1
2
3
4
5
6
7
8
9
10
/**
* 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_entry_t 用来存放被弱引用的对象(referent)和对该对象的所有弱引用(referrers),所以通过该对象就可以找到对其的所有弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
};
};

...
};

这几个对象之间的关系如下图所示:

这里比较关键的点就是以对象为key,以对该对象的所有弱引用为value,建立起了一层映射关系。通过去遍历weak_entries就可以拿到要找的weak_entry_t的index,进而拿到所有的弱引用。

__weak变量的初始化与销毁

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

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
/** 
* Initialize a fresh weak pointer to some object location.
* It would be used for code like:
*
* (The nil case)
* __weak id weakPtr;
* (The non-nil case)
* NSObject *o = ...;
* __weak id weakPtr = o;
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location Address of __weak ptr.
* @param newObj Object ptr.
*/
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** 
* Destroys the relationship between a weak pointer
* and the object it is referencing in the internal weak
* table. If the weak pointer is not referencing anything,
* there is no need to edit the weak table.
*
* This function IS NOT thread-safe with respect to concurrent
* modifications to the weak variable. (Concurrent weak clear is safe.)
*
* @param location The weak pointer address.
*/
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}

这两个函数都调用了storeWeak这个函数,但是传参不一样。storeWeak函数用了模板参数。

1
2
3
4
5
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。storeWeak的关键代码如下:

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
54
55
56
57
58
59
60
// Update a weak variable.
// If HaveOld is true, the variable has an existing value
// that needs to be cleaned up. This value might be nil.
// If HaveNew is true, there is a new value that needs to be
// assigned into the variable. This value might be nil.

// If CrashIfDeallocating is true, the process is halted if newObj is
// deallocating or newObj's class does not support weak references.
// If CrashIfDeallocating is false, nil is stored instead.
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
// haveNew为false时,newObj要为nil
if (!haveNew) assert(newObj == nil);

id oldObj;
SideTable *oldTable;
SideTable *newTable;

if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}

// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}

// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected

// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}

// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}

return (id)newObj;
}

haveOld为true时,要将弱引用移除掉,这里调用了weak_register_no_lock函数。这个函数通过oldObj找到对应的weak_entry_t,将entry->referrers释放掉,然后再释放掉该entry。

haveNew为true时,会用newObj和location创建一个新的entry,这里调用了weak_register_no_lock函数,如果原来有newObj的弱引用就追加到entry->referrers中,如果不存在就新添加一个。

__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修饰符的变量,则会消耗相应的CPU资源。良策是只在需要避免循环引用时使用__weak修饰符。
  • 如果大量使用附有__weak修饰符的变量,注册到 autoreleasepool 的对象也会大量增加,因此使用附有__weak修饰符的变量时,最好先暂时赋值给附有__strong修饰符的变量后再使用。
1
2
3
id weak o = obj;
id tmp = o;
NSLog(@"%@", tmp);

参考资料

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

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

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