对象的引用计数与dealloc

1,020 阅读10分钟

retain release在曾经的MRC时代经常活跃在我们眼前,现在的ARC时代我们很少见到他们了,但是不是说他们完全消失了,而是在编译阶段,编译器自动给我们插入了,下面我们就去看下他们的实现

首先我们要知道对象的引用计数都是报错在isaextra_rc与一个全局的SideTable的hash表中,然后我们先从retain方法去分析

找到retain方法的实现

- (id)retain {
    return ((id)self)->rootRetain();
}
id objc_object::rootRetain()
{
    return rootRetain(false, false);
}

这两个方法只是一个包装,然后看到有调用id objc_object::rootRetain(bool tryRetain, bool handleOverflow)方法,这个方法是整个retain的核心

id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);//加载isa
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // do not check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++ 将isa的值+1(isa中extra_rc+1)

        if (slowpath(carry)) {//extra_rc 不足以保存引用计数,
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {//并且 handleOverflow = false。走该循环条件
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);//重新执行rootRetain(bool tryRetain, bool handleOverflow)方法并将handleOverflow设置为true
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;//因为 extra_rc 已经溢出了,所以要更新它的值为 RC_HALF:二进制 10000000
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));//StoreExclusive(&isa.bits, oldisa.bits, newisa.bits) 更新 isa 的值

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

这个方法会分为两种情况,一种是在引用计数没有超出extra_rc的位数,在前面我们分析过extra_rc在arm64架构下为19位,一种是引用计数已经超出了extra_rc位数,如果没有超出那么情况很简单,只是单纯的进行extra_rc+1操作,其核心就是

newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  

这句代码就是将extra_rc进行加一,但是紧跟着我们可以看到又有一个if判断,这里面就是处理,如果引用计数超出所做的处理

     if (slowpath(carry)) {//extra_rc 不足以保存引用计数,
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {//1, handleOverflow = false。走该循环条件
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);//重新执行rootRetain(bool tryRetain, bool handleOverflow)方法并将handleOverflow设置为true
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }

首先判断了引用计数是否超出,然后在进行一次判断如果handleOverflow == false的时候直接return rootRetain_overflow(tryRetain);,我们上面可以看到,在调用rootRetain方法的时候, handleOverflow传入的就是false所以这个方法一定会走

 id 
objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, true);
}

rootRetain_overflow的方法很简单,就是将handleOverflow设置为true然后重新调用rootRetain方法,之后将extra_rc值设置为RC_HALF这个宏,并将has_sidetable_rctranscribeToSideTable设置为true

# if __arm64__
#       define RC_HALF  (1ULL<<18)
# elif __x86_64__
#       define RC_HALF  (1ULL<<7)

跳出循环后因为transcribeToSideTabletrue将调用bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)方法

if (slowpath(transcribeToSideTable)) {
     // Copy the other half of the retain counts to the side table.
     //将引用计数的一半保留到表中
     sidetable_addExtraRC_nolock(RC_HALF);
}

我们看下sidetable_addExtraRC_nolock的实现

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)//yty delta_rc = RC_HALF 1ULL<<18
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];//以对象地址为key获取对应的SideTable
	
	size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt =
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
	/*
	 delta_rc << SIDE_TABLE_RC_SHIFT
	 SIDE_TABLE_RC_SHIFT == 2   1ULL<<20 因为 refcnts 中的 64 为的最低两位是有意义的标志位,所以在使用 addc 时要将 delta_rc 左移两位,获得一个新的引用计数 newRefcnt。
	  64位的倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向); 64位的倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态) 其他的62位都可以用于存储retainCount.
	 */
	if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {//保存多余的retaincount
        refcntStorage = newRefcnt;
        return false;
    }
}

在这个方法中我们可以看到首先以对象本身的地址为key取出对应的SideTable

SideTable是一个全局的hash表,它里面存储了多出的引用计数与weak指针

然后从SideTable中的RefcountMap refcnts再以地址为key取出当前的引用计数refcntStorage,然后在原始的引用计数的基础上加上传入的引用计数<<2

向右偏移两位的原因是,RefcountMap refcnts的最后两位有特殊的标示意义
倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);
所以,64位环境下只有62位是保存溢出的引用计数的

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1) 

再往下我们可以看到,SideTable中的引用计数也是有可能会溢出的,这时候,就撤销这次的行为;否则将新的引用计数存储进去
由此我们可以得到,当对象的引用计数没有超出extra_rc时存储在extra_rc,而超出后则溢出的部分存储在SideTable

上面我们了解了retain是怎么对引用计数进行+操作的,下面我们去看看release对引用计数怎么进行-操作的,首先

- (oneway void)release {
    ((id)self)->rootRelease();
}
bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

retain类似rootRelease方法是整个release的核心

 bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);//获取isa
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--  将 isa 中的引用计数减一
        if (slowpath(carry)) {//如果是从SideTable借位了
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));//调用 StoreReleaseExclusive 方法保存新的 isa

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {//判断是否借助Sidetable存储引用计数
        if (!handleUnderflow) {//与retain作用相似 重新调用本方法(递归) rootRelease(bool performDealloc, bool handleUnderflow)并将handleUnderflow=true
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }
        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            goto retry;
        }
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);//试图从side table中删除计数  并返回所删除的引用计数
        if (borrowed > 0) {//借出的引用计数大于0
            // 尝试将引用计数放入extra_rc中
            newisa.extra_rc = borrowed - 1;  
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {//放入extra_rc失败
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }
            if (!stored) {//再试一次依旧不能将多余的引用计数放入isa中,于是重新将多余的引用计数在放入side table中
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }
            sidetable_unlock();
            return false;
        }
        else {
			//Side table是空的不需要做处理了 去做dealloc操作
        }
    }
    if (slowpath(newisa.deallocating)) {
		//当前对象正在释放
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
    }
    newisa.deallocating = true;//将deallocating设置为true 标志正在释放中
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();
    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);//如果可以释放 直接调用objc_msgSend调用dealloc方法
    }
    return true;
}

相对于retain操作,release就相对会复杂一些,首先判断是否是isTaggedPointer,如果是则return

Tagged Pointer 是对NSNumber NSDate等的一些的优化

然后还是分两大种情况一种是没有发生借位操作,只是将引用计数单纯的进行-1操作

uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); 

如果发生了借位,则又会分为两种情况,首先判断newisa.has_sidetable_rc是否为1,若为1则执行

      if (!handleUnderflow) {//与retain作用相似 重新调用本方法(递归) rootRelease(bool performDealloc, bool handleUnderflow)并将handleUnderflow=true
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

类似于retain时的操作
然后从SideTable中借出RC_HALF这么多位,然后将这个值-1后赋值给extra_rc

 size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);//试图从side table中删除计数 并返回所删除的引用计数
    if (borrowed > 0) {//借出的引用计数大于0
        newisa.extra_rc = borrowed - 1;  // redo the original decrement too
        bool stored = StoreReleaseExclusive(&isa.bits, 
                                            oldisa.bits, newisa.bits);
    }

下面紧跟着防止更新失败后再一次赋值操作,如果再次失败,则将数据重新放入表中;
第二种情况,如果Side table为空,则至今进行dealloc,首先将isadeallocating设置为true,然后直接调用dealloc方法。自此我们分析完了retainrelease的实现,那dealloc的时候又做了什么呢?

老套路,先看下delloc的方法调用

- (void)dealloc {
    _objc_rootDealloc(self);
}
void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

rootDealloc中可以看出来,如果这个isa是优化过的并且不包含/不曾经包含weak指针且没有关联对象且没有c++的析构方法且引用计数没有超出上限的时候可以快速释放,否则调用object_dispose方法

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

可以看到会调用移除关联对象的方法并且调用析构函数,然后调用了clearDeallocating方法

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

这个方法判断了是否是优化过后的isa,然后调用sidetable_clearDeallocatingclearDeallocating_slow,这两个方法都是对SideTable这个hash表进行一个清理,删除引用计数与weak表
那么我们自己在类中所写的属性是什么时候被释放的呢?我们去看下,首先我们先去看看object_cxxDestruct方法

void object_cxxDestruct(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    object_cxxDestructFromClass(obj, obj->ISA());
}

调用了object_cxxDestructFromClass

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.

    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            if (PrintCxxCtors) {
                _objc_inform("CXX: calling C++ destructors for class %s", 
                             cls->nameForLogging());
            }
            (*dtor)(obj);
        }
    }
}

这个方法中会从自己本身的类开始寻找.cxx_destruct方法,如果找到就调用,然后一直往上级的父类找循环调用,这个方法是C++的析构方法,我们的对象都是在这被释放的,那么这个.cxx_destruct方法是怎么出现在我们的类里面的? 写两个类

@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
@implementation Person
@end
@interface Student : Person
@property (nonatomic, strong) NSObject * objc;
@end
@implementation Student
- (void)setObjc:(NSObject *)objc{}
- (NSObject *)objc{ return  nil;}
@end

如果声明一个属性后自己手动将setter,getter方法写出来后,编译器不会将我们生成实例变量 然后测试

可以看出当类拥有自己的实例变量(非property)时,编译器会自动的给我们添加.cxx_destruct方法

最后在简单的说下weak吧,刚刚在看SideTable的时候可以看到,在SideTable中有RefcountMap refcnts;weak_table_t weak_table;两个,RefcountMap我们都知道是保存引用计数的,而weak_table_t正是保存当前对象有被哪些weak指针引用了

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
}

referent==>对象地址,用于weak_entry_t 数组遍历时的比对;

联合体内struct1->weak_referrer_t *referrers;
联合体内struct2->inline_referrers[WEAK_INLINE_COUNT]
weak变量的指针个数不超过4个用inline_referrers,
weak变量的指针个数超过4个用referrers.


小计:
1,对象的引用计数都是存储在extra_rcSideTable中;
2,对象拥有成员变量时编译器会自动插入.cxx_desctruct方法用于自动释放;
3,SideTable中保存了溢出的引用计数与weak指针


存疑:
在自身调试的时候发现如果子类的dealloc方法被调用后也会调用父类的dealloc,这是常识大家都知道,但是不理解为什么需要调用,dealloc中就解除了自己本身的关联对象,weak指针然后释放了所有成员变量,而且在释放成员变量的时候会向上找自己的父类,那么这时候调用[super dealloc]的话有什么意义吗?
已解决:调用[super dealloc]的原因是有可能父类中有在dealloc中处理一些别的事情,并且dealloc的操作是在顶级父类NSObject中实现的,如果不调用super就不会执行释放操作了(很简单的一个事情被我自己给搞的那么纠结...)


文章参考:
黑箱中的 retain 和 release
iOS Objective-C底层 part3:live^reference
iOS Objective-C底层 part4:die