前言

ARC中引入的weak关键字,用于修饰变量时写作__weak,也被称为空弱引用,意思是指在使用__weak修饰的指针指向的对象被销毁之后,指针会自动被置为nil.

clanggcc编译器下,__weak对应的类型属性为__attribute__((objc_ownership(weak)))。编译器根据__attribute__提供的属性来做相应的内存管理。

下面的测试代码使用XCode6.3.2,编译器使用默认的Apple LLVM version 6.1.0 (clang-602.0.53) (based on LLVM 3.6.0svn),参考代码来自苹果开源的 objc4-646.

weak的实现

例如有下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (id)obj {
return [[NSObject alloc] init];
}

- (void)test {
{
id obj = [self obj];
{
id __weak obj_weak = obj;
NSLog(@"%@",obj_weak);
NSLog(@"%@",obj_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
- (id) obj {
id id1 = ((id(*)(id, SEL))objc_msgSend)([NSObject class], @selector(alloc));
id id2 = ((id(*)(id, SEL))objc_msgSend)(id1, @selector(init));
return objc_autoreleaseReturnValue(id2);
}

- (void)test {
//{
id obj_tmp = ((id(*)(id, SEL))objc_msgSend)(self, @selector(obj));
id obj = objc_retainAutoreleasedReturnValue(obj_tmp);
//{
id obj_weak;
objc_initWeak(&obj_weak, obj);

objc_loadWeakRetained(&obj_weak);
_NSLog(obj_weak);
objc_release(obj_weak);

objc_loadWeakRetained(&obj_weak);
_NSLog(obj_weak);
objc_release(obj_weak);

objc_destroyWeak(&obj_weak);
//}
objc_storeStrong(&obj, nil);
//}
}

从上面的代码可以看到编译器在调用方法obj的过程中,先将返回值放进自动释放池,然后再从释放池中取出retain,看似很没有效率,其实编译器在这里是做了优化,如果objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue配套使用的话,编译器是没有将返回值放入自动释放池中的,详细过程可以参考文章后面的链接。

从上面代码也可以看出,每次使用一次weak变量,编译器都会做一次retainrelease的操作,另外对于另外一种情况,比如weak变量不是通过strong变量直接赋值,而是直接来自于方法的返回值,比如如果上面代码改为id __weak obj_weak = [self obj];,返回值是要放到自动释放池中的,这点也可以从objc_autoreleaseReturnValue的代码中分析得到。两种情况下,如果大量使用的话都会影响效率,所以使用weak变量的地方,显式转化为strong类型,是有助于提高性能的。

下面主要分析一下weak相关的函数调用,究竟做了一些什么事情

1
2
3
4
5
6
7
id objc_initWeak(id *, id)
-(id)objc_storeWeak(id *location, id newObj)
- oldTable = SideTable::tableForPointer(*location);//从全局散列表数组中,根据源指针(*location)指向的对象作为key,取出对应的散列表
- newTable = SideTable::tableForPointer(newObj);//相应的取出newObj对应的散列表,每个散列表都包含一个weak-table
- weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);//把location从老的weak-table中删除
- weak_register_no_lock(&newTable->weak_table, newObj, location); //将(location :newObj)存入新的weak-table
- newObj->setWeaklyReferenced_nolock()//将weak-table中标志位设为1,表示有weak引用

ARC下,编译器会生成一个全局的散列表数组,每个散列表的大小上限为128Byte,取这个数值也是为了和cache line size(64Byte)对齐,对于iOS系统,这个数组的大小为8*128(Byte),可以存放8张散列表。每张散列表中都包含有一个weak-table,weak-table中包含有一个存放weak_entry_t对象的哈希表(动态数组),系统能够根据对象的地址(key)而找到对应的结构体,weak_entry_t结构体包含了一张由weak指针的地址为key构成的哈希表,所以能根据对象地址找到其所有的弱引用,从而在对象被销毁之后全部设为nil。

API分析

1)tableForPointer

根据location作为key,从全局的散列表数组中选择一张表去存储location相应的信息。Apple采取的方法是这样的:假设全局散列表数组为table_buf[8],location唯一对应的散列表为

1
2
3
#define SIDE_TABLE_STRIPE 8
int index = ((a >> 4) ^ (a >> 9)) & (SIDE_TABLE_STRIPE - 1);
return table_buf[index];

这样可以避免一张表过于臃肿从而影响查找效率,类似的算法还有直接对SIDE_TABLE_STRIPE取余等,对于底层的算法效率是最重要的,Apple这个算法全部使用位运算,相比其他运算效率最高。
这种设计思想可以借鉴到我们的程序设计中,比如在数据库中要存储大量的用户数据,就可以根据用户的ID将用户信息分散存储在不同的表中。

2)weak_register_no_lock

函数调用顺序是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id weak_register_no_lock(&newTable->weak_table, newObj, location);
- weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
- size_t index = hash_pointer(referent) & weak_table->mask;
- while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
hash_displacement++;//哈希碰撞次数
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
- return &weak_table->weak_entries[index];
}
- if (entry) {
append_referrer(entry, referrer);
} else {
weak_entry_insert(weak_table, &new_entry);
}

上面代码的功能是向weak_table中注册一个weak引用,查找算法简单描述如下:对对象指针(原始key)做哈希运算,得到key,也就是数组的index,找到对应的weak_entry_t对象,如果key发生碰撞,则顺序查找下一个,直到找到为止。weak_entry_t也包含一张哈希表,以weak指针作为原始key得到index,如果这个index中有数据,即发生碰撞,顺序查找下一个,并将weak指针的地址存入这个位置,完成注册。如果没有找到相应饿entry对象,则新建一个并插入到weak_table中。

指针哈希算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
具体可以参考
http://locklessinc.com/articles/fast_hash/
http://floodyberry.com/noncryptohashzoo/
*/

#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#else
static inline uint32_t ptr_hash(uint32_t key)
{
key ^= key >> 4;
key *= 0x5052acdb;
key ^= __builtin_bswap32(key);
return key;
}
#endif

3)weak_unregister_no_lock

函数调用顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
id *referrer_id)

- weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);

- size_t index = hash_pointer(referent) & weak_table->mask;
- while (weak_table->weak_entries[index].referent != referent) {
index = (index+1) & weak_table->mask;
hash_displacement++;//哈希碰撞次数
if (hash_displacement > weak_table->max_hash_displacement) {
return nil;
}
- return &weak_table->weak_entries[index];
}
- if (entry){
remove_referrer(entry, referrer)
}
if (entry.empty) {
weak_entry_remove(weak_table, entry);
}

功能是从weak_table中解除weak指针的绑定。执行顺序正好和注册相反。

其他

介绍两个和weak相关的私有API

1
2
- (BOOL)allowsWeakReference;
- (BOOL)retainWeakReference;

在weak指针向weak_table的注册过程中,weak_register_no_lock会调用SEL_allowsWeakReference,如果重写这个方法返回NO,则不会进行上面的注册,直接返回nil。

在使用weak变量的过程中,objc_loadWeakRetained会调用SEL_retainWeakReference,如果重写这个方法返回NO,则读weak变量就会返回nil,但是如果直接返回YES,就会出问题,因为weak变量的retain操作是NSObject在- (BOOL)retainWeakReference的方法中实现的,所以重写这个方法时一定要注意要么返回NO,要么返回[super retainWeakReference],否则就会出现使用一次weak变量,所指向的对象就会被释放。

参考资料

Objective-C高级编程:iOS与OS X多线程和内存管理
Automatic Reference Counting(ARC)
黑幕背后的Autorelease