从 objc4-950 看 Objective-C 对象的创建与销毁流程
基于 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 这一段的主流程可以先粗略记成:
+alloc_objc_rootAlloccallAlloc_objc_rootAllocWithZone_class_createInstance_realizedmalloc_instance- 初始化
isa - 如果存在
C++构造,再执行构造逻辑 - 返回对象,继续执行
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/shiftcls、inline RC 等状态一起写进去,方便后续 retain/release/析构走更快的路径。再配合 fastpath/slowpath 和 ALWAYS_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 流程,我在代码里做了标注。粗略地讲,可以分成下面几步:
-
在
MRC、桥接,或者类自己接管了引用计数逻辑的场景里,可能会自定义-retain/-release,所以 runtime 会先通过hasCustomRR判断要不要走自定义路径。如果没有自定义,就直接进入rootRelease的内联计数 / 侧表逻辑;如果有,则优先判断能否走Swift的引用计数路径,能走就调用swiftRelease,否则发送release消息。 -
接着通过
slowpath(!oldisa.nonpointer)进入 raw isa 的慢路径,并进一步判断它对应的是不是元类;如果是元类,就不走普通实例释放逻辑。 -
接着做 非指针 isa 的内联计数递减:原子加载
isa,尝试extra_rc--。若成功且未转入“正在销毁”,释放结束;若借不到计数则进入“下溢”处理。 - 然后进行下溢处理(借还侧表的引用计数):若
has_sidetable_rc为真,持锁从侧表借回一半计数[sidetable_subExtraRC_nolock(RC_HALF)]并重做递减;若侧表为空,转入“正在销毁”状态。 - 进入“正在销毁”状态:当内联计数为 0,且侧表里也没有可借的引用计数时,开始进入
performDealloc。
这一段可以先记成下面这条主线:
release- 先走内联引用计数递减
- 如果内联计数不够,再去
SideTable借 - 内联计数和侧表计数都归零后,进入 deallocating
- 调用
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
SideTable是runtime里的“侧表”数据结构,用来在对象头之外存储引用计数、销毁标志和弱引用信息。- 设计目标是配合非指针
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(是否正在销毁),以及一小部分内联引用计数等等。
这样做的好处很直接:很多判断都可以在对象头里通过一次位运算完成,不必总去查侧表,也不必额外发消息。