基于 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,如果是则直接返回 nilslowpath是告知 CPU 这个分支这个情况极少出现。

然后在 fastpath 分支中,大多数类不会重写 +alloc/+allocWithZone(AWZ),因此直接走底层 C 语言分配函数,绕过 objc_msgSend

然后接着判断 if (allocWithZone) ,假如该类自定义了 AWZ(allocWithZone),就必须尊重类的实现,通过消息发送调用[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_AWZclass_rw_t.flags 中的同义语义位,存储在 rw 视图里;当不启用快缓存位的情况下,就用它作为信息来源。

  • 小结: 这部分是对象创建流程中,优化最大的一部分,slowpath/fastpath 是分支预测宏,用来强化“nil 很少”“无自定义 AWZ 很多”的假设,从而让绝大多数类的分配走直通 C 实现,既快又不改变语义,跳过了多次 objc_msgSend;而有自定义 AWZ 的类则保留消息发送以保证行为一致性。

_objc_rootAllocWithZone

其余几个分支都是走 objc_msgSend 方法,所以可以单独来看它

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

这里 cls->realizeIfNeeded() 是在分配前确保类已经 realize(加载并准备好 ro/rw 元数据、方法缓存等)。

然后接着进入下一步:

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);快速获取内存对齐后的 size,接着统一使用objc::malloc_instance来分配对象。

接着来看

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

这里 fast 表示“这个类的实例是否允许使用非指针 isa。举个例子,为了加快速度,isa 字段里面可能以位域形式承载更多状态信息(如是否有 C++ 析构、是否被弱引用、是否正在释放、内联引用计数等),这就是非指针 isa;如果不被允许,则使用指针 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;
}

这里就算完成了对象的初始化流程。

总结

在最新版本下,一个对象的初始化时,绝大部分 alloc 方法会跳过 objc_msgSend,直接走 C 函数分配;同时在 fast 路径下,一次写入非指针 isa,携带 has_cxx_dtor、可能的 index/shiftclsinline RC 等状态,后续 retain/release/析构更快;通过分支预测宏 fastpath/slowpathALWAYS_INLINE 帮助编译器/CPU 把热点路径紧凑布局,提升冷启动与普遍分配性能。

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,所以要通过 hasCustomRR 来进行判断。如果没有自定义:直接向下走 rootRelease 的内联计数/侧表逻辑(最快路径);若有自定义,则先判断是否可以走 Swift 引用计数,能走则调用 swiftRelease,若无,则发送 release 消息.

  2. 接着通过 slowpath(!oldisa.nonpointer)判断是否是元类,如果是,则不做实例释放。

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

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

dealloc

在执行完 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 方法的时候用到,这里不做深入研究。

我们进入 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
}
  • 这里作者非常幽默的写下了一个注释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 加锁,调用 weak_clear_no_lock 将所有弱变量置 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

  • SideTable 是运行时的“侧表”数据结构,用来在对象头以外存储引用计数、销毁标志和弱引用信息。
  • 设计目标是配合非指针 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)。然后当非指针 isa 自身标注的引用计数清零的时候,会看这个标注( has_sidetable_rc),发现在侧表中存了引用计数,然后将它取出.

每次会取一半,主要原因是为了性能。因为如果要逐个去取,每次都要都一遍流程,过于频繁,如果取太多,则会不必要地搬迁大量计数;一次取一半,则会减少性能损耗。

其次也要考虑到内存的安全性,对象被视为“正在销毁”必须同时满足“内联计数为 0 且侧表计数为 0”。借回之后会更新 has_sidetable_rc 位以反映剩余侧表计数,确保销毁判定与真实计数一致。

非指针 isa

为了优化性能,存在着大量的非指针 isa。

runtime 会把大量的信息,比如说 has_assoc(是否有/曾有关联对象)、has_cxx_dtor(是否有 C++/ObjC 析构)、weakly_referenced(是否被弱引用过)、has_sidetable_rc(是否有侧表计数)、deallocating(是否正在销毁),小型内联引用计数等等,编入 非指针 isa 中。 这带来的好处是很多判断可以直接在对象头一次位操作完成,避免查侧表或发消息。