源码阅读之Weak关键字的实现原理

2018-07-15  本文已影响58人  蛋壳儿

对于修饰符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实例,然后studenttclass属性为弱引用,如下

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.msetTclass中打个断点,然后进入,可以看到调用栈

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打上断点,可以看到如下图所示的调用栈

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表中的相关记录。
注意:

4.总结

那么我们简单总结一下weak的具体步骤

  1. 当给一个weak属性赋值时,会根据被赋值的对象地址为key,当前指针的地址为value在全局的weak表中注册一条记录,如果注册表中当前指针之前已经指向了一个旧的对象,那么先把之前的那条删除,再添加新的
  2. weak指向的对象销毁时,系统会根据之前对象的地址再weak表中查询是否有弱引用的指针记录,然后将所有的指针置为nil
  3. 可见使用weak时,在赋值与对象销毁的过程中会产生很多额外的操作,在对性能有极限可以考虑使用__unsafe_unretained,当然是不影响使用的前提下(比如YYCache源码中,作者在实现链表的nextpre指针时,就用的__unsafe_unretained来代替weak

水平有限,文中如有出现错误,敬请指出

上一篇下一篇

猜你喜欢

热点阅读