分析 runtime 创建销毁
基于 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(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_AWZ是 class_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/shiftcls、inline RC 等状态,后续 retain/release/析构更快;通过分支预测宏 fastpath/slowpath 和 ALWAYS_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 流程,我在代码中进行了标注,粗略的讲一下,
-
当在
MRC或者桥接的场景化,有可能自定义了-retain/-release,所以要通过hasCustomRR来进行判断。如果没有自定义:直接向下走rootRelease的内联计数/侧表逻辑(最快路径);若有自定义,则先判断是否可以走Swift引用计数,能走则调用swiftRelease,若无,则发送release消息. -
接着通过
slowpath(!oldisa.nonpointer)判断是否是元类,如果是,则不做实例释放。 -
接着做 非指针 isa 的内联计数递减:原子加载
isa,尝试extra_rc--。若成功且未转入“正在销毁”,释放结束;若借不到计数则进入“下溢”处理。 - 然后进行下溢处理(借还侧表的引用计数):若
has_sidetable_rc为真,持锁从侧表借回一半计数[sidetable_subExtraRC_nolock(RC_HALF)]并重做递减;若侧表为空,转入“正在销毁”状态。 - 进入“正在销毁”状态:当内联计数为 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 中。
这带来的好处是很多判断可以直接在对象头一次位操作完成,避免查侧表或发消息。