NSOiOARC中weak实现 2015-06-28
前言
ARC
中引入的weak
关键字,用于修饰变量时写作__weak
,也被称为空弱引用,意思是指在使用__weak
修饰的指针指向的对象被销毁之后,指针会自动被置为nil
.
在clang
和gcc
编译器下,__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 | - (id) obj { |
从上面的代码可以看到编译器在调用方法obj
的过程中,先将返回值放进自动释放池,然后再从释放池中取出retain
,看似很没有效率,其实编译器在这里是做了优化,如果objc_autoreleaseReturnValue
和objc_retainAutoreleasedReturnValue
配套使用的话,编译器是没有将返回值放入自动释放池中的,详细过程可以参考文章后面的链接。
从上面代码也可以看出,每次使用一次weak变量,编译器都会做一次retain
和release
的操作,另外对于另外一种情况,比如weak
变量不是通过strong
变量直接赋值,而是直接来自于方法的返回值,比如如果上面代码改为id __weak obj_weak = [self obj];
,返回值是要放到自动释放池中的,这点也可以从objc_autoreleaseReturnValue
的代码中分析得到。两种情况下,如果大量使用的话都会影响效率,所以使用weak变量的地方,显式转化为strong
类型,是有助于提高性能的。
下面主要分析一下weak
相关的函数调用,究竟做了一些什么事情1
2
3
4
5
6
7id 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 | #define SIDE_TABLE_STRIPE 8 |
这样可以避免一张表过于臃肿从而影响查找效率,类似的算法还有直接对SIDE_TABLE_STRIPE
取余等,对于底层的算法效率是最重要的,Apple这个算法全部使用位运算,相比其他运算效率最高。
这种设计思想可以借鉴到我们的程序设计中,比如在数据库中要存储大量的用户数据,就可以根据用户的ID将用户信息分散存储在不同的表中。
2)weak_register_no_lock
函数调用顺序是
1 | id weak_register_no_lock(&newTable->weak_table, newObj, location); |
上面代码的功能是向weak_table中注册一个weak引用,查找算法简单描述如下:对对象指针(原始key)做哈希运算,得到key,也就是数组的index,找到对应的weak_entry_t对象,如果key发生碰撞,则顺序查找下一个,直到找到为止。weak_entry_t也包含一张哈希表,以weak指针作为原始key得到index,如果这个index中有数据,即发生碰撞,顺序查找下一个,并将weak指针的地址存入这个位置,完成注册。如果没有找到相应饿entry对象,则新建一个并插入到weak_table中。
指针哈希算法如下:
1 | /* |
3)weak_unregister_no_lock
函数调用顺序如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void 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 | - (BOOL)allowsWeakReference; |
在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