内存管理(四)引用计数与weak

449 阅读7分钟

在前面的文章中,已经对引用计数以及其在开发中的使用做了初步了解。在本篇中,我们将会深入阐述苹果对引用计数这个技术的底层实现。

本文涉及到的objc源码,来自于objc源码,版本是723,文中涉及大量源码,均做删减,主要对加锁部分进行删减。

导读:

  1. 对源码解读部分,若觉得源码冗余,每一步解读最后都有总结,可直接看总结。
  2. 第五部分-图总结了引用计数相关的图。

一、引用计数的存储

我们知道,对于纯量类型的变量,是没有引用计数的,因为不是对象,其申请的变量存储在栈上,由系统来负责管理。

另外一种类型,是前面讲过的Tagged Pointer小对象,包括NSNumberNSStringNSDate等几个类的变量。它们也是存储在栈上,当然也没有引用计数。

整理了下面表格:

关于对象类型的引用计数,其存在的地方有两个:

1.1 isa指针里的引用计数

首先,我们来观察对象类型中存放在优化后的isa指针,下面是出现过多次的isa指针布局:

1.2 Side Table里的引用计数

Side Table在系统中的结构如下:

而每一张Side Table中针对引用计数的部分如下:

二、引用计数管理

在了解了引用计数存储的地方之后,对引用计数管理的理解就更方便了。

2.1 管理引用计数的方法

下面,针对上面的方法,根据源码,进行一定的追踪。

2.2 retainCount

后续引用计数相关方法,只看优化过的指针,即只针对64位进行描述。

根据源码,其调用轨迹如下:

NSObject.mm ━retainCount()     ┗━ rootRetainCount()           ┗━ rootRetainCount()

inline uintptr_t objc_object::rootRetainCount()
{
    // 1. 如果是Tagged Pointer,直接返回
    if (isTaggedPointer()) return (uintptr_t)this;
    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {  //优化的isa指针
        //2. 取出isa中的retainCount+1
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            //3. 如果Side Table还有存储,则取出累加返回
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

retainCount总结

其中,针对后两种情况:

对于存在于Side Table中的引用计数,需要注意,其最低2位被占用,所以取出来,要右移2位。

2.3 retain

源码调用路径如下:

NSObject.mm ━retain()     ┗━objc_retain()       ┗━obj->retain()                     ┗━rootRetain()

ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // 1. Tagged Pointer对象直接返回
    if (isTaggedPointer()) return (id)this;
    bool sideTableLocked = false;
    // transcribeToSideTable用于表示extra_rc是否溢出,默认为false
    bool transcribeToSideTable = false;
    isa_t oldisa;
    isa_t newisa;
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);   // 将isa_t提取出来
        newisa = oldisa;
        // 如果对象正在析构,则直接返回nil
        // don't 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;
        // 2. isa中指针extra_rc+1
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // 有进位,表示溢出,extra_rc已经不能存储在isa指针了
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                // 如果不处理溢出情况,则在这里会递归调用一次,再进来的时候
                // handleOverflow会被rootRetain_overflow设置为true,从而直接进入到下面的溢出处理流程
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }

            /*
               溢出处理:
               1. 先将extra_rc中引用计数减半,继续存储在isa中。
               2. 同时把has_sidetable_rc设置为true,表明借用了Side Table存储。
               3. 将另一半的引用计数,存放到Side Table中.
             */
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); // 将oldisa 替换为 newisa,并赋值给isa.bits(更新isa_t), 如果不成功,do while再试一遍

    if (slowpath(transcribeToSideTable)) {
        // 3. isa的extra_rc溢出,将一半的refer count值放到Side Table中(RC_HALF = 18,extra_rc一共19位)
        // 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;
}

retain总结:

Tagged Pointer对象,没有retain。

isa中extra_rc若未溢出,则累加1。如果溢出,则将isa和side table对半存储。

2.4 release

源码调用路径:

NSObject.mm ━release()      ┗━ objc_release()                   ┗━ obj->release()                                ┗━ rootRelease()

下面是release方法的主要执行流程:

ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    // 1. 如果是Tagged Pointer,直接返回false,不需要dealloc
    if (isTaggedPointer()) return false;
    isa_t oldisa;
    isa_t newisa;
 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        uintptr_t carry;
        // 2. 将当前isa指针中存储的引用计数减1,如果未溢出,返回false,不需要dealloc
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // 3. 减1导致下溢出,则需要从Side Table借位,跳转到下溢出处理
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    return false;
 underflow:
    newisa = oldisa;
    if (slowpath(newisa.has_sidetable_rc)) {
        // Side Table存在引用计数
        if (!handleUnderflow) {
            // 如果不处理溢出,此处只会调用一次,handleUnderflow会被rootRelease_underflow设置为ture,
            // 再次进入,则会直接进入溢出处理流程
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }
        // 3.1 尝试从Side table取出 当前isa能存储引用计数最大值的一半 的引用计数
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);
        // 如果取出引用计数大于0
        if (borrowed > 0) {
            // 3.2 将取出的引用计数-1,并保存到isa中,保存成功返回false(未被销毁)
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // 3.2.1 保存到isa中失败,再次尝试保存
                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) {
                // 3.2.2 如果还是保存失败,则将借出来的一半retain count,重新返回给Side Table
                // 并且重新从 2 开始尝试
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }
            return false;
        } else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
    // 4. 以上流程执行完了,则需要销毁对象,调用dealloc
    if (slowpath(newisa.deallocating)) {
        // 对象正在销毁
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
    __sync_synchronize();
    // 销毁对象
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}


// Equivalent to [this autorelease], with shortcuts if there is no override
inline id 
objc_object::autorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}

release总结:

三、对象的销毁

3.1 dealloc方法

3.2 dealloc重写

3.3 dealloc源码

源码的执行流程如下:

━ dealloc ┗━_objc_rootDealloc —— <NSObject.mm> ┗━ rootDealloc —— <objc-object.h>              ┗━ object_dispose —— <objc-runtime-new.mm>                          ┗━ objc_destructInstance、free  —— <objc-runtime-new.mm>

我们跟踪方法rootDealloc

inline void objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
    // 假如是非指针的isa,即优化的isa
    // 且对应的没有关联对象、weak应用、以及c++析构函数和side_table的引用计数,就直接free释放掉。
    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);
    }
}

下面是具体销毁对象的流程:

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();   // 将指向当前的弱指针置为nil
    }

    return obj;
}

是的,下一步就是进入clearDeallocating()方法。

四、weak

weak有一个特性,在对象销毁的时候,指向该对象所有的weak指针都会置为nil,那么这个特性是如何在dealloc方法里体现的。

4.1 weak指针存储对象结构

要了解weak指针的处理,先要了解其对象结构。

weak指针存储在一个个Side Table对象中的weak_table,这是一张hash表。

之后,在weak_table中,存储着一个个对象的entry表,这也是个hash表,每个entry就存储着要销毁对象的所有弱引用地址。

Side Table在系统有一组,可根据对象地址获取其对应的SideTable。

如下图所示:

4.2 weak指针存储的hash表

上面说道weak指针的对象结构,如下图,展示的是weak指针如何通过hash表串联起来,进行存储的。

4.3 源码

下面,我们就继续跟踪上面说到的clearDeallocating()方法,根据优化过的isa指针,其中调用的:

━ clearDeallocating()   —— <objc-object.h>            ┗━ clearDeallocating_slow()  —— <NSObject.mm>

NEVER_INLINE void objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        // 对象有weak指针指向,将会全部置为nil
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        // 引用计数存储在Side Table中,对引用计数进行擦除
        table.refcnts.erase(this);
    }
    table.unlock();
}

更进一步,看看是如何清空weak指针的。

void weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
    //1. 拿到被销毁对象的指针
    objc_object *referent = (objc_object *)referent_id;
    //2. 通过hash查找对象指针在weak_table中查找出对应的entry
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        return;
    }
    // 3. 将所有的引用设置成nil
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        // 3.1. 如果弱引用超过4个则将referrers数组内的弱引用都置成nil。
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } else {
        // 3.2. 不超过4个则将inline_referrers数组内的弱引用都置成nil
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    // 循环设置所有的引用为nil
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
        }
    }
    // 4. 从weak_table中移除referent_id对应对象的entry
    weak_entry_remove(weak_table, entry);
}

weak指针处理总结:

五、图

5.1 对象结构图

5.2 存储图

参考

链接

  1. Advanced Memory Management Programming Guide
  2. Memory Management Programming Guide for Core Foundation
  3. Automatic Reference Counting
  4. Objective-C Automatic Reference Counting (ARC)
  5. Friday Q&A 2010-07-16: Zeroing Weak References in Objective-C
  6. Friday Q&A 2017-09-22: Swift 4 Weak References
  7. runtime 如何实现 weak 属性
  8. Objective-C 引用计数原理
  9. iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-一
  10. iOS管理对象内存的数据结构以及操作算法--SideTables、RefcountMap、weak_table_t-二
  11. objc源码