源码阅读之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实例,然后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表中的相关记录。
注意:

  • objc_destroyWeak是在自己销毁调用的
  • weak引用的对象销毁时调用的是weak_clear_no_lock来将所有指向自己的weak指针置为nil

4.总结

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容