Objective-C 小记(7)retain & release

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

retain

retain 在现在的 runtime 中的默认实现是 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 里去
  • oldisaisa 本来的值
  • newisaisa 新的值(增加了引用计数后的值)
    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 的实现也在 objc-object.h 中:

// 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 上,溢出后转移到一个表里。感觉是个很有意思的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容