详解获取weak对象的过程

答案

这里假设,此对象不是TaggedPointer对象,除了一些必要的判断外,在ARC中,获取weak指针时,会调用objc_loadWeakRetained,此方法最终会调用objc_object::rootRetain,对该对象的引用计数器加1,然后在此条语句的下面插入一条release语句,对引用计数器减1,在MRC中,会调用objc_autorelease(objc_loadWeakRetained(location));,利用objc_autorelease对引用计数器减1.

为什么我会有这个疑问?

最近复习了OC内存管理的相关知识,在查阅相关知识时,看到了这篇文章weak指针的线程安全和自动置nil的深度探讨,作者提出了一个下面这个比较有意思的问题?

weak指针会自动置为nil的原因就是在一个对象的delloc中会去弱引用表里面查找所存储weak指针的数组,然后去遍历置为nil。相信这个结论家都比较认同。但是,如果一个类重写delloc方法,且设置为MRC并不调用super delloc。也就是说这个类必定不能顺利的完成delloc,并不能把指针置为nil,但是当获取weak指针的时候,weak指针却神奇地为nil。难道之前的结论是错误的?

对于这个问题,作者给出的答案是:获取weak的指向为nil,其真是的弱引用表可能没有清空,或者正在被清空,但我们取值weak指针地值是nil,始作俑者是objc_loadWeakRetained方法。会直接返回nil给我们使用,其真正的弱指针还是存在的,还是指向该对象的。在runtime源码里面追踪retainWeakReference地实现,最终来的了objc_object::rootRetain函数,猜想:应该是isa指针的是否正在被delloc的位域起了作用。如果一个对象被标记为正在被delloc,那么获取其weak指针会被直接返回nil。与其weak指针的真身无关。

网上分析objc_loadWeakRetained源码的文章比较多,这里只贴出来,就不分析了,

id objc_loadWeakRetained(id *location) {
    id obj;id result;Class cls;
    SideTable *table;
 retry:
    obj = *location;
    if (!obj) return nil;
    if (obj->isTaggedPointer()) return obj;
    table = &SideTables()[obj];
    table->lock();
    if (*location != obj) {
        table->unlock();
        goto retry;
    }
    result = obj;
    cls = obj->ISA();
    if (!cls->hasCustomRR()) {
        if (! obj->rootTryRetain()) {
            result = nil;
        }
    }
    else {
        if (cls->isInitialized() || _thisThreadIsInitializingClass(cls)) {
            BOOL (*tryRetain)(id, SEL) = (BOOL(*)(id, SEL))
                class_getMethodImplementation(cls, SEL_retainWeakReference);
            if ((IMP)tryRetain == _objc_msgForward) {
                result = nil;
            }
            else if (! (*tryRetain)(obj, SEL_retainWeakReference)) {
                result = nil;
            }
        }
        else {
            table->unlock();
            _class_initialize(cls);
            goto retry;
        }
    }
    table->unlock();
    return result;
}

对于作者的答案我是认同的,不过我认为作者那么说不全面,我认为的流程是这样的


屏幕快照 2019-12-11 上午12.10.19.png
  1. 获取weak指针时,会调用objc_loadWeakRetained
  2. 判断weak指向的对象是否是isTaggedPointer,若是:直接返回该对象
  3. 判断ISA->hasCustomRR(),我与作者的分歧就在着,此比特位会在该类或父类重写下列方法时retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference返回true,一般情况我们都不会重写这些方法,因此会返回false,取反就为true
  4. 那么下一步就会执行if (! obj->rootTryRetain()) { result = nil; },尝试对该对象进行retain
  5. 若retain成功则返回该对象
  6. 若retain失败,则会返回nil,

obj->rootTryRetain(),这个方法最终会调用objc_object::rootRetain(bool tryRetain, bool handleOverflow),并且参数为true,false,在这个函数里有一下判断:当tryRetain为true,并且对象为被标记为deallocating时,会返回nil

if (slowpath(tryRetain && newisa.deallocating)) {
    ClearExclusive(&isa.bits);
    if (!tryRetain && sideTableLocked) sidetable_unlock();
    return nil;
}

当然作者的那个答案也没有错,是因为作者重写了retainWeakReference方法,让hasCustomRR为true,会走下面的else,并且作者把retainWeakReference直接返回了YES,那么最终会返回该对象。只是该对象被标记为deallocating,并没有真正的被释放。

这个时候我又产生了一个新的疑问,既然获取weak修饰的对象,会调用objc_loadWeakRetained方法,而此方法最终又会调用rootRetain对该对象的引用计数器加1,那么什么时候对该对象的引用计数器减1的呢?

创建一个可调试的objc-runtime的工程,在rootRetain方法中添加打印语句printf("rootRetain\n");,在rootRelease方法中添加打印语句printf("rootRelease\n");,然后测试下面代码

@interface Person : NSObject
@property (nonatomic, assign)NSInteger age;
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __weak Person *weakPerson1;
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
        weakPerson1.age = 18;
        printf("第一次retain-release\n");
        weakPerson1.age = 18;
        printf("第二次retain-release\n");
        weakPerson1.age = 18;
        printf("第三次retain-release\n");
        weakPerson1.age = 18;
        printf("第四次retain-release\n4");
        NSLog(@"hello world");
    }
    return 0;
}

打印结果如下


屏幕快照 2019-12-10 下午11.23.34.png

针对结果,每次操作weak指向的对象都会对该对象进行一次retain和release,retain是在objc_loadWeakRetained中经过层层调用rootRetain方法添加的,那release如何调用的呢?

这时候分为2种情况:

  1. 在ARC中编译器会在weak对象操作的下面插入一条release语句,weakPerson1.age = 18;相当于weakPerson1.age = 18;object_release(obj)
  2. 在MRC中,获取weak指向的对象时,并不会直接调用objc_loadWeakRetained,而是会调用objc_loadWeak,此方法的实现如下:利用自动释放池,对retain的对象进行release操作
id objc_loadWeak(id *location) {
    if (!*location) return nil;
    return objc_autorelease(objc_loadWeakRetained(location));
}

关于weak的另一个问题

在提出问题之前,你首先要了解weak_table_tweak_entry_t以及weak的基本原理。我们知道weak_entry_t是一个存放着某个对象所有的弱引用列表,是一个类数组对象。那么下面2种情况,weak_entry_t的长度分别是多少?
第一种情况:

    __weak Person *weakPerson1;
    @autoreleasepool {
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
    }
    // 在此时,对象obj会被释放,在释放的过程会把所有指向它的弱引用都置为nil
    // 此时存储弱引用的weak_entry_t的长度是多少

第二种情况:

    __weak Person *weakPerson1;
    __weak Person *weakPerson2;
    __weak Person *weakPerson3;
    __weak Person *weakPerson4;
    __weak Person *weakPerson5;
    @autoreleasepool {
        Person *obj = [[Person alloc]init];
        weakPerson1 = obj;
        weakPerson2 = obj;
        weakPerson3 = obj;
        weakPerson4 = obj;
        weakPerson5 = obj;
    }
    // 在此时,对象obj会被释放,在释放的过程会把所有指向它的弱引用都置为nil
    // 此时存储弱引用的weak_entry_t的长度是多少
    return 0;

答案是,第一种情况是4,第二种情况是8,原因是weak_entry_t在存储的弱引用的个数小于4的时候,使用的是内联数组,直接初始化了4个位置,也就是说当小于4时,长度会一直是4,每次需要删除某一个弱引用时,都会对数组进行遍历,查找到该引用进行置nil,需要添加时,会遍历此数组,看有没有为nil的,若有,就代表有空位,就赋值,若没有就代表此内联的数组已经满了,此时会把内联数组转成哈希表,哈希表的默认长度为8.weak_entry_tout_of_line用来标记是否使用内联数组。

参考

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

推荐阅读更多精彩内容