答案
这里假设,此对象不是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;
}
对于作者的答案我是认同的,不过我认为作者那么说不全面,我认为的流程是这样的
- 获取weak指针时,会调用objc_loadWeakRetained
- 判断weak指向的对象是否是isTaggedPointer,若是:直接返回该对象
- 判断ISA->hasCustomRR(),我与作者的分歧就在着,此比特位会在该类或父类重写下列方法时retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference返回true,一般情况我们都不会重写这些方法,因此会返回false,取反就为true
- 那么下一步就会执行
if (! obj->rootTryRetain()) { result = nil; }
,尝试对该对象进行retain - 若retain成功则返回该对象
- 若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;
}
打印结果如下
针对结果,每次操作weak指向的对象都会对该对象进行一次retain和release,retain是在objc_loadWeakRetained
中经过层层调用rootRetain
方法添加的,那release如何调用的呢?
这时候分为2种情况:
- 在ARC中编译器会在weak对象操作的下面插入一条release语句,
weakPerson1.age = 18;
相当于weakPerson1.age = 18;object_release(obj)
- 在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_t
和weak_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_t
中out_of_line
用来标记是否使用内联数组。