6 minute read

基于 objc4-950 版本。

上一篇文章我从对象结构的角度分析了一些 runtime 源码,顺带看到了新版本里的一些优化;这次换一个角度,从对象的生命周期来看看创建和销毁到底是怎么走的。

初始化流程

一般来说,我们使用下面这种方式创建一个对象:

id obj = [[MyClass alloc] init];

很简单,直接从源码来看。

+ (id)alloc {
    return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

以上这些和之前版本差不多,但是接下来的内容,在新版本里有一些比较明显的优化:

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

在这里,runtime 先做一个判断:检查 cls 是否为 nil。如果是,就直接返回 nil;而 slowpath 的作用,是告诉 CPU 这个分支极少出现。

然后在 fastpath 分支里,大多数类都不会重写 +alloc/+allocWithZone(这里简称 AWZ),因此可以直接走底层的 C 分配函数,绕过一次 objc_msgSend

接着判断 if (allocWithZone)。如果这个类自定义了 AWZ,那就必须尊重类自己的实现,通过消息发送去调用 [cls allocWithZone:nil];如果不是这个分支,那就走 [cls alloc]

这里的 hasCustomAWZ 是一个标志位:

#if FAST_CACHE_HAS_DEFAULT_AWZ
    bool hasCustomAWZ() const {
        return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
    }
#else
    bool hasCustomAWZ() const {
        return !(bits.data()->flags & RW_HAS_DEFAULT_AWZ);
    }

FAST_CACHE_HAS_DEFAULT_AWZ 表示这个类(或其父类)使用的是默认的 +alloc/+allocWithZone 实现。这个位存储在元类的“快速缓存”结构中,读取开销很低,适合放在分配入口这种热路径里使用;RW_HAS_DEFAULT_AWZ 则是 class_rw_t.flags 中表达同样语义的标志位,存储在 rw 视图里,在不启用快缓存位时就用它来提供信息。

  • 小结: 这部分是对象创建流程里优化最明显的一块。slowpath/fastpath 本质上是分支预测宏,用来强化“nil 很少出现”“没有自定义 AWZ 的类很多”这两个假设,从而让绝大多数类的分配直接走到底层 C 实现,既快又不改变语义;而对那些自定义了 AWZ 的类,则继续保留消息发送,保证行为一致。

alloc 这一段的主流程可以先粗略记成:

  1. +alloc
  2. _objc_rootAlloc
  3. callAlloc
  4. _objc_rootAllocWithZone
  5. _class_createInstance_realized
  6. malloc_instance
  7. 初始化 isa
  8. 如果存在 C++ 构造,再执行构造逻辑
  9. 返回对象,继续执行 init

_objc_rootAllocWithZone

其余几个分支都会走 objc_msgSend,所以这里可以把 _objc_rootAllocWithZone 单独拎出来看。

id _objc_rootAllocWithZone(Class cls, objc_zone_t)
{
    cls->realizeIfNeeded();
    return _class_createInstance_realized(cls, 0, OBJECT_CONSTRUCT_CALL_BADALLOC);
}

这里 cls->realizeIfNeeded() 的作用,是在真正分配实例之前,确保这个类已经完成了必要的 realize 工作,比如把运行时需要的类元数据准备好。

然后接着进入下一步:

static ALWAYS_INLINE id
_class_createInstance_realized(Class cls, size_t extraBytes,
                               int construct_flags = OBJECT_CONSTRUCT_NONE,
                               bool cxxConstruct = true,
                               size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj = objc::malloc_instance(size, cls);
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

在这里,我们先把 hasCxxCtor/hasCxxDtor/canAllocNonpointer 读到局部变量里,减少重复读取,也方便编译器做寄存器优化。

然后通过 size = cls->instanceSize(extraBytes); 拿到内存对齐后的实例大小,接着统一使用 objc::malloc_instance 来分配对象。

接着来看

    if (fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        obj->initIsa(cls);
    }

这里的 fast 表示“这个类的实例是否允许使用非指针 isa”。简单理解的话,非指针 isa 会在对象头里顺便塞进更多位信息,比如是否有 C++ 析构、是否被弱引用过、是否正在释放、内联引用计数等等;如果不允许使用这种形式,那就退回到传统的指针 isa,里面主要只放类指针,其他状态则更多依赖 SideTable,用一点性能去换兼容性和安全性。

接着往下,

if (fastpath(!hasCxxCtor)) {
    return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);

如果类没有 C++ 构造函数(hasCxxCtor 为假),直接返回对象;如果有 C++ 构造,则调用 object_cxxConstructFromClass.

接着返回上层调用链,继续执行 init

- (id)init {
    return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
    return obj;
}

这里可以认为“分配 + 默认初始化”这一段已经走完了。对于 NSObject 的默认实现来说,_objc_rootInit 基本不做额外工作,就是把对象本身返回出去。

小结

在最新版本下,一个对象的初始化过程中,绝大部分 alloc 会尽量跳过 objc_msgSend,直接走底层 C 分配;同时在 fast 路径下,一次初始化非指针 isa,把 has_cxx_dtor、可能的 index/shiftclsinline RC 等状态一起写进去,方便后续 retain/release/析构走更快的路径。再配合 fastpath/slowpathALWAYS_INLINE,热点路径会更紧凑,整体分配性能也会更好。

dealloc

接着来探究一下对象的销毁流程。

我们知道 iOS 默认使用的是自动引用计数,所以 dealloc 这条链路,本质上是从引用计数归零开始的。

release

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant) {
    if (slowpath(isTaggedPointer())) return false;
    bool sideTableLocked = false;

    isa_t newisa, oldisa;

    oldisa = LoadExclusive(&isa().bits);
    1----------1
    if (variant == RRVariant::FastOrMsgSend) {

        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa().bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }
    1----------1
    
    2----------2
    if (slowpath(!oldisa.nonpointer)) {
        // a Class is a Class forever, so we can perform this check once
        // outside of the CAS loop
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa().bits);
            return false;
        }
    }
    2----------2
    
    3----------3
#if !ISA_HAS_INLINE_RC
    // Without inline ref counts, we always use sidetables
    ClearExclusive(&isa().bits);
    return sidetable_release(sideTableLocked, performDealloc);
#else
retry:
    do {
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits);
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }

        // 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(newisa.isDeallocating()))
        goto deallocate;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;
3----------3

4----------4
 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 (variant != RRVariant::Full) {
            ClearExclusive(&isa().bits);
            return rootRelease_underflow(performDealloc);
        }

        // 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.
            oldisa = LoadExclusive(&isa().bits);
            goto retry;
        }

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

        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement too
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);

            if (!stored && oldisa.nonpointer) {
                // 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.
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                ClearExclusive(&isa().bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa().bits);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }
4----------4
5----------5
deallocate:
    // Really deallocate.

    ASSERT(newisa.isDeallocating());
    ASSERT(isa().isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        this->performDealloc();
    }
    5----------5
    return true;
#endif // ISA_HAS_INLINE_RC
}

这是一段非常复杂的 release 流程,我在代码里做了标注。粗略地讲,可以分成下面几步:

  1. MRC、桥接,或者类自己接管了引用计数逻辑的场景里,可能会自定义 -retain/-release,所以 runtime 会先通过 hasCustomRR 判断要不要走自定义路径。如果没有自定义,就直接进入 rootRelease 的内联计数 / 侧表逻辑;如果有,则优先判断能否走 Swift 的引用计数路径,能走就调用 swiftRelease,否则发送 release 消息。

  2. 接着通过 slowpath(!oldisa.nonpointer) 进入 raw isa 的慢路径,并进一步判断它对应的是不是元类;如果是元类,就不走普通实例释放逻辑。

  3. 接着做 非指针 isa 的内联计数递减:原子加载 isa,尝试 extra_rc--。若成功且未转入“正在销毁”,释放结束;若借不到计数则进入“下溢”处理。

  4. 然后进行下溢处理(借还侧表的引用计数):若 has_sidetable_rc 为真,持锁从侧表借回一半计数 [sidetable_subExtraRC_nolock(RC_HALF)] 并重做递减;若侧表为空,转入“正在销毁”状态。
  5. 进入“正在销毁”状态:当内联计数为 0,且侧表里也没有可借的引用计数时,开始进入 performDealloc

这一段可以先记成下面这条主线:

  1. release
  2. 先走内联引用计数递减
  3. 如果内联计数不够,再去 SideTable
  4. 内联计数和侧表计数都归零后,进入 deallocating
  5. 调用 performDealloc

performDealloc

在执行完 release 流程后,会进入

void objc_object::performDealloc()
{
    if (ISA()->hasCustomDeallocInitiation())
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(_objc_initiateDealloc));
    else
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}

这里的 _objc_initiateDealloc 对应的是自定义 dealloc initiation 的那条路径,这里先不展开。

我们先进入 runtime 自动处理的这条 dealloc 路径。

- (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 !ISA_HAS_INLINE_RC
    _object_dispose_nonnull_realized((id)this);
#else
    if (fastpath(isa().nonpointer                     &&
                 !isa().weakly_referenced             &&
                 !isa().has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa().has_cxx_dtor                  &&
#else
                 !isa().getClass(false)->hasCxxDtor() &&
#endif
                 !isa().has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        _object_dispose_nonnull_realized((id)this);
    }
#endif // ISA_HAS_INLINE_RC
}
  • 这里 runtime 作者还挺有意思地留了一个注释:if (isTaggedPointer()) return; // fixme necessary?,意思大概是“这行到底是不是真的有必要”。因为按理论来说,不应该给 Tagged Pointer 发送 -dealloc

如果是非指针 isa,并且同时满足:无弱引用无关联对象无 C++ 析构无侧表 RC,那就可以直接 free(this);否则就进入“完整销毁”流程。

完整销毁流程

id _object_dispose_nonnull_realized(id obj)
{
    objc_destructInstance_nonnull_realized(obj);
    free(obj);

    return nil;
}

static void *objc_destructInstance_nonnull_realized(id 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_associations(obj, /*deallocating*/true);
    obj->clearDeallocating();
    return obj;
}

这里主要做了几件事:

  • 若类有 .cxx_destruct,执行 object_cxxDestruct
  • 若对象存在关联对象,移除所有关联 _object_remove_associations(obj, true)
  • 清理“正在销毁”状态与侧表/弱引用残留 clearDeallocating()

重点看 clearDeallocating

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

    assert(!sidetable_present());
}

  • 非指针 isa:如果存在弱引用或者侧表计数,就走 clearDeallocating_slow,加锁后把所有弱引用清成 nil,并清除侧表相关条目。
  • 原始指针 isa:走 sidetable_clearDeallocating,同样会把弱引用置 nil,并移除对应的侧表条目。

释放内存

完整销毁返回后,由 _object_dispose_nonnull_realized 调用 free(obj)

id
_object_dispose_nonnull_realized(id obj)
{
    objc_destructInstance_nonnull_realized(obj);
    free(obj);

    return nil;
}

释放掉内存之后,到这里一个对象的完整销毁流程就走完了。

其他

SideTable

  • SideTableruntime 里的“侧表”数据结构,用来在对象头之外存储引用计数、销毁标志和弱引用信息。
  • 设计目标是配合非指针 isa 的内联计数和标志位,承载溢出的引用计数、跨线程安全的弱引用映射,以及为 raw isa 对象(没有内联位)提供完整的计数存储。

结构如下:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void reset() { slock.reset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template<HaveOld, HaveNew>
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template<HaveOld, HaveNew>
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

}

SideTable 提供 lock/unlock 以及双表有序加锁帮助弱引用赋值避免死锁。主要作用是:

  • 减少对象头大小与分支复杂度 非指针 isa 的位域有限,不能长期容纳很大的引用计数;额外计数放入侧表,保持对象头紧凑与位操作快速。
  • 提升并发性能与可扩展性 通过分片(StripedMap)把不同对象的侧表锁分散,减少全局竞争;弱表/引用计数的操作在各自分片锁下进行,避免热点锁。
  • 支持原始 isa 对象 某些对象使用原始 isa(不带位域),其引用计数与弱标志必须完全依赖侧表;因此侧表提供完整的 retain/release/tryRetain/retainCount/清理等接口。
  • 统一弱引用生命周期管理 弱指针不参与计数,但必须在对象销毁时一致置 nil,侧表集中管理弱引用的位置列表,确保清理正确且线程安全。

对于第一点,可以这样理解:

非指针 isa 本身可以携带一小部分引用计数,但为了控制对象头大小,当引用计数继续增长时,多出来的部分就会放进侧表里,同时打上 has_sidetable_rc 这个标记。等到内联计数快耗尽时,runtime 会看到这个标记,再去侧表里把计数借回来。

每次取一半,主要还是出于性能考虑。如果一次只取一个,那就要频繁重复同一套流程;如果一次取太多,又会做不必要的大量搬迁。折中地取一半,整体会更合适。

另外还要考虑销毁判定本身的正确性:一个对象只有在“内联计数为 0 且侧表计数也为 0”的前提下,才能真正进入销毁状态。借回计数之后,也要同步更新 has_sidetable_rc,保证这个状态和真实引用计数保持一致。

非指针 isa

为了优化性能,现代 runtime 大量使用了非指针 isa

runtime 会把很多信息塞进非指针 isa 里,比如 has_assoc(是否有 / 曾有关联对象)、has_cxx_dtor(是否有 C++ / ObjC 析构)、weakly_referenced(是否被弱引用过)、has_sidetable_rc(是否存在侧表计数)、deallocating(是否正在销毁),以及一小部分内联引用计数等等。

这样做的好处很直接:很多判断都可以在对象头里通过一次位运算完成,不必总去查侧表,也不必额外发消息