详解获取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源码的文章比较多,这里只贴出来,就不分析了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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

  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

1
2
3
4
5
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
");
,在rootRelease方法中添加打印语句printf("rootRelease
");
,然后测试下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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
");
        weakPerson1.age = 18;
        printf("第二次retain-release
");
        weakPerson1.age = 18;
        printf("第三次retain-release
");
        weakPerson1.age = 18;
        printf("第四次retain-release
4");
        NSLog(@"hello world");
    }
    return 0;
}

打印结果如下

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


针对结果,每次操作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操作
1
2
3
4
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的长度分别是多少?
第一种情况:

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

第二种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    __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的深度探讨
  4. 理解 ARC 实现原理