本文使用的 runtime 版本为 objc4-706

retain

retain 在现在的 runtime 中的入口是 objc_retain 函数,可以在 NSObject.mm 中找到它的实现:

__attribute__((aligned(16)))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

可以看到,它将实际工作交给了 objc_object 结构体的 retain 函数,可以在 objc-object.h 中找到它:

// Equivalent to calling [this retain], with shortcuts if there is no override
inline id 
objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

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

retain 函数首先断言对象指针不是一个 tagged pointer(assert(!isTaggedPointer())),之后对 isa 中是否有自定义 retainrelease 实现标示位进行判断,如果没有自定义的实现,则进入默认实现 rootRetain 函数,否则的话直接向对象发送 retain 消息,调用自定义的 retain 实现。

本文的关注点当然是在默认实现上,所以继续查看 rootRetain 函数的实现:

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

rootRetain 函数的实现是调用了另一个重载的 rootRetain

在继续对下面的代码进行分析之前,先回顾一下 isa 的结构(这里只对 x86-64 架构的 isa_t 进行分析):

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };
};

Objective-C 小记(2)对象 2.0 中有对 isa_t 更详细的描述。现在需要关心的是 has_sidetable_rcextra_rc 这两个位字段(bit-field)。extra_rc 表示「额外的 retain count」,假如 extra_rc 的值为 2,则对象的引用计数为 3。回顾 Objective-C 小记(6)alloc & init 可以发现,对象在创建时 extra_rc 的值是 0,引用计数则是 1。还可以注意到 extra_rc 只有 8 位,这样它最多能记到 255,如果这个时候引用计数还要往上增加怎么办呢?这时候对象会将一半的引用计数存储到一个表里,并将 has_sidetable_rc 置为 1。

回到 rootRetain 函数:

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

函数一开始又检查了自己是不是 tagged pointer(if (isTaggedPointer()) return (id)this;),这难道就是防御式编程?

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

它首先声明了四个变量,四个变量都能从名字知道它们的用处:

sideTableLocked 用来表示 side table 是否锁上了
transcribeToSideTable 用来表示是否需要将 isa 中的引用计数转移到 side table 里去
oldisa isa 本来的值
newisa isa 新的值(增加了引用计数后的值)
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        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();
        }
        // 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;
        }

之后进入 do-while 循环,循环里首先将 transcribeToSideTable 赋值为 falseoldisanewisa 赋值为 isa.bits 的值(LoadExclusive 的作用是让读取操作原子化,根据 CPU 不同实现不同,比如在 x86-64 上就是单纯的直接返回值,而在 arm64 上则使用了 ldxr 指令)。

关于 slowpathfastpath 宏,在 Objective-C 小记(6)alloc & init 中有解释。

关于 tryRetain,这个参数与 weak 的实现有关,本文暂不做分析。

首先会检查 isa 是不是 non-pointer(if (slowpath(!newisa.nonpointer)) { ... }),如果不是 non-pointer,就进入 sidetable_retain 这个过程,这是完全由一个表来存放引用计数的实现。

第二个判断则是和 tryRetain 有关,暂时不做分析。可以发现这两个判断使用的都是 slowpath,表示是不太可能出现的情况。

        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // 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;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

接下来就是重点部分了,声明 carry 变量来标示是否溢出。然后使用 addc(newisa.bits, RC_ONE, 0, &carry)newisaextra_rc 位字段加 1。这里有个判断是否溢出,如果溢出的话还要判断 handleOverflow 是否为 true,可以注意到这个函数被调用时 hadleOverflowfalse,需要进入 rootRetain_overflow 函数,而 rootRetain_overflow 的实现是这样的:

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

它又重新调用 rootRetain,不过将 handleOverflow 置为了 true,希望有大神分享一下为什么要这样做……rootRetain 里剩余的工作也很好理解,将 side table 锁住,给 sideTableLockedtranscribeToSideTable 设置好值,extra_rc 留下一半(在 x86-64 下就是 126)的引用计数,并将 has_sidetable_rc 设置为 true

最后 while 里的操作是对比 isaoldisa 的值,如果一样则将 newisa 覆盖 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;
}

最后,函数检查 transcribeToSideTable,也就是如果之前的操作有溢出,则将一半的引用计数加到表里。

release

release 的入口也在 NSObject.mm 中:

__attribute__((aligned(16)))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

同样,它也将实际工作交给了 objc-object 结构体。

// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

retain 基本上是一致的,如果有自定义实现的话,则发消息,否则进入默认实现 rootRelease

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

套路真是一模一样,继续看 rootRelease 的实现:

ALWAYS_INLINE 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);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }

同样也是进入一个 do-while 循环,套路满满,这里也不解释了。

        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

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

使用 subc(newisa.bits, RC_ONE, 0, &carry)newisa 的引用计数减 1,发现下溢出后跳转到 underflow。如果没有溢出,函数就这样结束了。继续看 underflow 的代码:

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

首先将 newisa 重制,然后判断这个对象有没有 side table,有的话,可以把 side table 里的引用计数移过来。但判断里面又是判断 handleUnderflow 这个参数,rootRelease_underflow 的实现也是和 rootRetain_overflow 差不多的:

NEVER_INLINE bool 
objc_object::rootRelease_underflow(bool performDealloc)
{
    return rootRelease(performDealloc, true);
}

总之调用了这个函数还是会回到上面的代码,就继续往下看吧:

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

首先将 side table 锁住,为了防止出现竞争又跑一遍 retry

        // Try to remove some retain counts from the side table.        
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                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) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

这里从 side table 借 RC_HALF 的引用计数放到 extra_rc 上。接下来的代码是从 side table 借不到的情况,那当然就是对象需要被销毁了。

    // Really deallocate.

    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;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}

可以看到,就是直接就发送了 dealloc 消息。

总结

对于现在的 non-pointer isa 来说,引用计数一部分存储在 isa 的 extra_rc 上,溢出后转移到一个表里。感觉是个很有意思的实现。