源码阅读之Weak关键字的实现原理
对于修饰符weak
想必大家都比较熟悉了,比起assign
和__unsafe_unretained
,weak
使用时更加安全,因为它会在弱引用的对象销毁时,自动把当前指针置为nil。那weak到底是怎么完成这个工作的呢,本文结合源码(runtime源码可以从这里下载)做一些探讨。
用一个runtime
调试环境能更好的帮助我们去一步步的探索,有兴趣的同学可以点此下载,本文使用的版本为objc-723。
1. 我们先写两个类,以班级和学生为例,Student类中有一个弱引用属性班级,如下
//tclass.h
@interface TClass : NSObject
@end
//student.h
@interface Student : NSObject
@property (nonatomic, weak) TClass *tclass;
@end
//student.m
@implementation Student
- (void)setTclass:(TClass *)tclass
{
_tclass = tclass;
}
@end
现在开始运行我们的代码,在main.m中创建一个student
实例和一个tclass
实例,然后student
的tclass
属性为弱引用,如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *a = [[Student alloc] init];
//在这里加个autoreleasepool是为了观察tclass释放时,系统把a.tclass置为nil的过程
@autoreleasepool {
TClass *tclass = [[TClass alloc] init];
a.tclass = tclass;
NSLog(@"%@",a.tclass);
}
NSLog(@"%@",a.tclass);
}
return 0;
}
2. weak
属性存储
在student.m
的setTclass
中打个断点,然后进入,可以看到调用栈
出现了
objc_storeWeak(id*,id)
方法的调用,我们继续点进源码,可以看到调用了方法
static id storeWeak(id *location, objc_object *newObj)
相当于把a.tclass的指针和tclass的地址传进来,将指针指向tclass,源代码中storeWeak(id*location, objc_object *newObj)
方法的实现很长,主要就是做了两件事,我写了一段伪代码
storeWeak(id *location,objc_object *newObjc)
{
//1.判断location是不是已经在全局的weak表作用注册过,也就是说这个若引用指针是不是之前已经指向过某个对象了
//这个时候拿当前弱引用的指针指向的对象的地址,当做key在weak表中找自己
//(可能有点绕,以此例子说就是student a之前已经在班级1中,所以a.tclass指针之前指向的是班级1
//同时weak表中已经有以班级1为key的一条记录,里面有这个a同学,这个时候a又换到班级2中
//那么所做的就是把班级1中a同学的记录删除)
id oldObjc = *location;
if (weak_is_registered_no_lock(weak_table, oldObj,location))
{
//如果找到了就删除旧的值,这里*location想当于班级1,location是a.class指针
weak_unregister_no_lock(weaktable, oldObj, location);
}
//2.存储新的weak关系,以newObjc的地址为key,把a.class的指针地址作value存储起来
weak_register_no_lock(weaktable, newObj, location);
}
在源码objc_weak.h
文件中,有上面的方法具体实现,主要有这四个方法
<!--objc-weak.h-->
<!--定义了一个全局的weak表,提供以下四种方法-->
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
//添加一个键值对到weak 表中
id weak_register_no_lock(weak_table_t *weak_table,id referent,id *referrer,bool crashIfDeallocating)
//根据地址删除一个键值对
id weak_unregister_no_lock(weak_table_t *weak_table,id referent, id *referret)
//返回一个布尔值,某个对象是否被若引用注册到了weak表中
id weak_is_registered_no_lock(weak_table_t *weak_table,id referent)
//当一个对象调用析构函数时,把所有的若引用置为nil
void weak_clear_no_lock(weak_table_t *weak_table,id referrent)
先来看看weak_register_no_lock
方法,源代码如下
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating)
{
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
// 在这删除了一部分代码
weak_entry_t *entry;
if ((entry = weak_entry_for_referent(weak_table, referent))) {
append_referrer(entry, referrer);
}
else {
weak_entry_t new_entry(referent, referrer);
weak_grow_maybe(weak_table);
weak_entry_insert(weak_table, &new_entry);
}
return referent_id;
}
源码很长,贴上去的删除了部分代码,删除的代码主要是做一些可行性判断,主要看下面的代码,先去判断弱引用指向的对象在没在weak表中注册过,如果注册过,则在key为对象地址的值后面(最开始时,key对应的是一个数组),追加新的弱引用指针地址append_referrer(entry, referrer)
,这个方法里面也很有意识,先判断指向这个对象的弱引用指针是否超过了4个,如果没超过则直接往后加
//WEAK_INLINE_COUNT为4
for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
if (entry->inline_referrers[i] == nil) {
entry->inline_referrers[i] = new_referrer;
return;
}
}
如果超过了4个,那么就会将之前的数组转化为hash表,用来提升后续针对此对象的查找,清除等操作的速度。
如果此对象的地址,之前没有再weak表中注册过,则调用生成一条记录,然后插入。类似的还有一个对weak表是否需要扩容的判断,weak表其实也是一个hash表,如果表中的key已经超过了当时设计的3/4,也就是hash的负载因子大于0.75时则进行扩容if (weak_table->num_entries >= old_size * 3 / 4) { weak_resize(weak_table, old_size ? old_size*2 : 64); }
关于hash表的效率,扩容等可查看bs的博客深入理解hash表
3. 看完了weak
的存储,那么下面再看看weak
是怎么在对象dealloc
时,将之前的指针地址赋值nil
的
还是之前的代码,在TClass.m
中的dealloc
打上断点,可以看到如下图所示的调用栈
可以看到当
TClass
销毁时,会调用到weak_clear_no_lock
,看看这个方法
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == referent) {
*referrer = nil;
}
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
在tclass
销毁时,查找weak
表,如果有以此对象的地址为key的注册记录,那么就把指向自己的所有的弱引用指针置为nil
,然后再调用weak_entry_remove
方法,删除此条记录,同时判断weak
表是不是需要重新设置大小
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
}
add: 我看到过很多文章中写道,weak
引用的对象销毁时,会调用objc_destroyWeak
方法,然后调用storeWeak(location,nil)
来让weak
置为nil
来保证weak
指针的安全,其实不是这样的!!!,objc_destroyWeak
方法是在自己销毁时会调用,以本文中的例子来说,就是当student的对象a
销毁时,会调用objc_destroyWeak
方法之后调用storeWeak(*location,nil)
,再调用weak_unregister_no_lock
方法去删除weak
表中的相关记录。
注意:
-
objc_destroyWeak
是在自己销毁调用的 -
weak
引用的对象销毁时调用的是weak_clear_no_lock
来将所有指向自己的weak
指针置为nil
4.总结
那么我们简单总结一下weak
的具体步骤
- 当给一个
weak
属性赋值时,会根据被赋值的对象地址为key,当前指针的地址为value在全局的weak
表中注册一条记录,如果注册表中当前指针之前已经指向了一个旧的对象,那么先把之前的那条删除,再添加新的 - 当
weak
指向的对象销毁时,系统会根据之前对象的地址再weak
表中查询是否有弱引用的指针记录,然后将所有的指针置为nil - 可见使用
weak
时,在赋值与对象销毁的过程中会产生很多额外的操作,在对性能有极限可以考虑使用__unsafe_unretained
,当然是不影响使用的前提下(比如YYCache
源码中,作者在实现链表的next
和pre
指针时,就用的__unsafe_unretained
来代替weak
)
水平有限,文中如有出现错误,敬请指出