【iOS】retain/release底层实现原理
前言
在前面OC的学习中,我们了解到了OC中的关键字,今天我们来具体解析一下strong、copy、retain、release的实现原理,他们都是内存管理的核心机制,其底层实现深度依赖引用计数(Retain Count)和运行时(Runtime)的 SideTable机制。
前情知识
在了解其底层原理之前,我们先来回顾一下引用计数的相关概念。
OC内存管理的核心是引用计数(ARC 下由编译器自动管理,MRC 下手动操作)。每个对象有一个隐式的引用计数retainCount),表示当前有多少个“拥有者”持有该对象:
- 引用计数 +1:对象被“持有”(如
retain
、strong
赋值)。 - 引用计数 -1:对象被“释放”(如
release
、strong
变量超出作用域)。 - 引用计数为 0:对象被销毁(调用
dealloc
),内存被回收。
OC对象的内存生命周期由引用计数控制。每个对象的内存头部(objc_object结构体)都存储了一个isa指针,以及一个隐藏的引用计数字段(retainCount)。当引用计数变为0时,对象被销毁(调用dealloc),内存被回收。
引用计数的存储方式
- 小对象优化:对于小对象(如NSNumber、NSNull等小尺寸对象)
NSNumber:基本数据类型(如 int、float、BOOL等)的对象化包装
NSNull:空值的对象化表示。NSNull是一个特殊的类,用于表示“空值”(类似其他语言的 null或 None)
- 大对象:对于大对象(如NSObject子类、NSArray等),引用计数存储在全局的SideTable中。SideTable是一个哈希表,通过对象的地址作为键,映射到包含引用计数和弱引用表的条目(side_table_t)。
retain和release的实现原理(MRC手动管理)
在MRC模式下,retain和release是手动管理对象生命周期的核心方法,直接操作引用计数。
retain(MRC手动管理)
retain源码
retain的作用是增加对象的引用计数,确保对象在被持有期间不会被释放。关于retain这部分的源码,在objc4_906_main中如下:
inline id
objc_object::retain()
{//确保对象不是小对象,如果是,就直接返回自身,因为小对象的值直接编码在指针地址中,无需堆分配,不用引用计数ASSERT(!isTaggedPointer());return rootRetain(false, RRVariant::FastOrMsgSend);
}
在retain内联的rootRetain函数前面,还有一个其的无参数版本:
ALWAYS_INLINE id
objc_object::rootRetain()
{return rootRetain(false, RRVariant::Fast);
}
这段无参数的rootRetain方法版本,核心作用是通过内联调用简化代码,并为特定场景(如内部优化路径)提供更高效的调用方式,快速触发对象的引用计数增加功能操作。
内联函数rootRetain源码
进入rootRetain后,源码如下:
ALWAYS_INLINE id
//rootRetain是retain操作的核心实现,负责处理引用计数的原子性增加、多线程安全、内联引用计数与侧表的转录等
//参数:tryRetain为false,表示这是一个普通的保留操作,而非尝试保留(尝试保留通常用于锁机制中,避免阻塞)
// RRVariant::FastOrMsgSend是一个枚举值,指示当前调用路径是快速路径或模拟objc_msgSend发送retain消息的场景
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{//检查对象是否为小对象(如 int、BOOL、小 NSNumber等)。小对象的值直接编码在指针地址中(无需堆分配),无需引用计数管理if (slowpath(isTaggedPointer())) return (id)this;bool sideTableLocked = false; // 标记侧表是否已锁定(避免多线程竞争)bool transcribeToSideTable = false; // 标记是否需要将内联计数转录到侧表isa_t oldisa; // 保存旧的 isa 位域状态isa_t newisa; //新的 isa 位域状态(待更新)oldisa = LoadExclusive(&isa().bits); //原子加载当前 isa 位域的原子性(避免其他线程修改导致脏读)if (variant == RRVariant::FastOrMsgSend) { //表示当前调用是“快速路径”或“模拟 objc_msgSend发送 retain消息”(如直接调用 [obj retain])// These checks are only meaningful for objc_retain()// They are here so that we avoid a re-load of the isa.if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) { //检查对象的类是否覆盖了自定义的引用计数逻辑(如 retain/release方法);若有,跳过默认逻辑,直接调用自定义实现ClearExclusive(&isa().bits); // 释放原子加载的锁if (oldisa.getDecodedClass(false)->canCallSwiftRR()) { //判断是否可调用 Swift 的保留函数(swiftRetain),兼容 Swift 对象的引用计数管理return swiftRetain.load(memory_order_relaxed)((id)this);}return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));}}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 (id)this;}}//编译器宏,指示是否启用内联引用计数(现代 iOS/macOS 系统默认启用):若未启用内联引用计数(如旧版本或特殊配置),引用计数完全存储在侧表(SideTable)中,直接调用侧表的 tryRetain或 retain方法
#if !ISA_HAS_INLINE_RC// No need for a CAS loop in this case; we aren't changing the ISA pointerClearExclusive(&isa().bits);if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;else return sidetable_retain(sideTableLocked);
#elsedo {transcribeToSideTable = false;newisa = oldisa;if (slowpath(!newisa.nonpointer)) { // 重新检查是否为非指针 isa(可能被其他线程修改)ClearExclusive(&isa().bits);if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;else return sidetable_retain(sideTableLocked);}// don't check newisa.fast_rr; we already called any RR overridesif (slowpath(newisa.isDeallocating())) { //对象正在释放,禁止保留ClearExclusive(&isa().bits);if (sideTableLocked) {ASSERT(variant == RRVariant::Full);sidetable_unlock();}if (slowpath(tryRetain)) {return nil;} else {return (id)this;}}uintptr_t carry;newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ 原子增加extra_rcif (slowpath(carry)) { //内联引用计数溢出(extra_rc超过最大值)// newisa.extra_rc++ overflowedif (variant != RRVariant::Full) { //非完整变体,直接处理溢出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))); //CAS重新提交isa位域if (variant == RRVariant::Full) {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(); //非尝试保留时解锁侧表} else {ASSERT(!transcribeToSideTable);ASSERT(!sideTableLocked);}
#endifreturn (id)this;
}
从上述代码中,我们可以知道objc_object::rootRetain
的工作流程以原子性增加对象引用计数为核心,代码执行的流程大致如下:
首先初始化 sideTableLocked
和 transcribeToSideTable
状态变量,并通过 LoadExclusive
原子加载当前 isa 位域(oldisa)。随后进入 do-while
循环,通过 StoreExclusive
原子提交新 isa 位域(newisa),确保多线程下的原子性操作。循环中检查 newisa 是否为非指针 isa(若为非指针则重置锁并重试),并根据 tryRetain 标志选择调用 sidetable_tryRetain
(尝试保留)或 sidetable_retain
(强制保留)。若对象正在释放(isDeallocating()
),则清除锁并根据条件返回 nil或对象自身。若内联引用计数(extra_rc)溢出(carry 标志为真),则将部分计数转录到侧表(标记 has_sidetable_rc
)并调整内联值(保留 RC_HALF)。最终提交 isa 更新后,同步侧表数据(若为完整变体)并解锁,返回对象自身完成引用计数增加。
相关的sidetable_tryRetain()方法
接下来我们再来看涉及到的sidetable_tryRetain()方法的源码:
bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA //编译器宏,指示是否支持非指针 isa(如小对象优化的 Tagged Pointer)ASSERT(!isa().nonpointer); //若支持非指针 isa,则当前对象的 isa不能是指针类型(nonpointer标志为 false)。此断言用于确保非指针 isa场景下,侧表操作的正确性(避免对非指针 isa执行侧表逻辑)
#endifSideTable& table = SideTables()[this];//SideTables():全局哈希表,存储每个对象地址对应的 SideTable(通过对象地址哈希映射)。SideTable结构:每个 SideTable包含两个核心结构:refcnts:引用计数映射表(RefcountMap),记录对象被弱引用或临时引用的次数;weak_table_t:弱引用表,存储指向该对象的弱引用指针(如 __weak变量)。// NO SPINLOCK HERE// _objc_rootTryRetain() is called exclusively by _objc_loadWeak(), // which already acquired the lock on our behalf.// fixme can't do this efficiently with os_lock_handoff_s// if (table.slock == 0) {// _objc_fatal("Do not call -_tryRetain.");// }bool result = true;//原子操作,尝试在 refcnts中插入当前对象的条目。若条目已存在(it.second为 false),则直接获取现有条目;若不存在(it.second为 true),则插入新条目,初始值为 SIDE_TABLE_RC_ONE(表示一次弱引用)auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);auto &refcnt = it.first->second;//根据 try_emplace的结果(it.second)和当前引用计数的状态(refcnt),分三种情况处理:if (it.second) { //情况一:插入新条目// there was no entry} else if (refcnt & SIDE_TABLE_DEALLOCATING) { //情况二:条目已存在且对象正在被释放result = false;} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) { //情况三:条目已存在且对象未被释放refcnt += SIDE_TABLE_RC_ONE;}return result;
}
由此,我们可以看来,objc_object::sidetable_tryRetain()
函数通过侧表(SideTable)尝试为对象添加弱引用,流程如下:
首先断言确保对象为指针类型(非小对象优化场景),获取对象对应的侧表;通过原子操作 try_emplace
尝试在侧表的引用计数映射(refcnts
)中插入当前对象的条目。若插入成功(新条目),初始化引用计数为 SIDE_TABLE_RC_ONE
(一次弱引用);若条目已存在,检查对象状态:若对象正在释放(deallocating
标志),则尝试失败;若未被固定(pinned 标志),则将引用计数加 1。最终返回是否成功尝试保留对象(true或 false)。
retain底层工作流程总结
至此,我们可以知道retain底层的工作流程图大致如下:
+----------------------+
| objc_retain() |
+----------------------+|v
+--------------------------+
| 检查对象是否为 nil? |
+--------------------------+|+-------+-------+| |是(nil) 否| |
返回 nil +-----------------------------+| 调用 obj->retain() |+-----------------------------+|v+----------------------------------------+| objc_object::retain() || || -> 是否启用 Tagged Pointer? || -> 是否使用 isa-optimized 引用计数? |+----------------------------------------+|+------------+--------------+| |isa优化计数 YES NO+-----------------------------+ +-----------------------------+| 利用 isa 指针中位域计数器 | | 使用 SideTable 哈希表 || 直接在 isa 中的引用计数 +1 | | sidetable_retain() |+-----------------------------+ +-----------------------------+|v+----------------------------------+| 加锁 -> SideTable.refcnts[obj]+1 || 解锁 |+----------------------------------+
release
release的作用是减少对象的引用计数,若计数归零则销毁对象。release的底层逻辑:
- 检查对象是否为小对象,若是则直接返回。
- 获取 SideTable并减少引用计数。
- 若引用计数减至 0:调用 dealloc方法(释放实例变量、关联对象等);释放 SideTable内存(若无其他对象使用)。
release源码
在objc_906_main中,这部分源码如下:
inline void
objc_object::release()
{ASSERT(!isTaggedPointer()); // 断言非小对象rootRelease(true, RRVariant::FastOrMsgSend); // 调用核心释放函数
}inline void
objc_object::release()
{ASSERT(!isTaggedPointer()); // 断言非小对象// 快速路径:无自定义释放逻辑时,直接操作侧表if (fastpath(!ISA()->hasCustomRR())) { // 元类无需释放(无实例指向)if (!ISA()->isMetaClass())sidetable_release(); // 释放侧表中的引用计数return;}// 自定义释放逻辑:通过消息发送调用类的 release 方法((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}
内联函数rootRelease源码
这部分源码有很多地方的处理与rootRelease相同,有的不再一一赘述。
ALWAYS_INLINE bool
objc_object::rootRelease()
{return rootRelease(true, RRVariant::Fast);
}ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{if (slowpath(isTaggedPointer())) return false;bool sideTableLocked = false; // 标记侧表是否锁定(避免多线程竞争)isa_t newisa, oldisa; //保存新旧 isa 位域状态oldisa = LoadExclusive(&isa().bits); // 原子加载当前 isa 位域(多线程安全)if (variant == RRVariant::FastOrMsgSend) { //RRVariant::FastOrMsgSend:表示当前调用是“快速路径”或“模拟 objc_msgSend发送 release消息”(优化调用流程)// These checks are only meaningful for objc_release()// They are here so that we avoid a re-load of the isa.if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) { //这里跟rootRetain的判断逻辑一样,兼容OC和swiftClearExclusive(&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;}}if (slowpath(!oldisa.nonpointer)) { //元类检查(跟roorRetain一样)// a Class is a Class forever, so we can perform this check once// outside of the CAS loopif (oldisa.getDecodedClass(false)->isMetaClass()) {ClearExclusive(&isa().bits);return false;}}#if !ISA_HAS_INLINE_RC// Without inline ref counts, we always use sidetablesClearExclusive(&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 overridesuintptr_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;//引用计数溢出处理(借位逻辑)underflow:// newisa.extra_rc-- underflowed: borrow from side table or deallocate(借位:从侧表借位或触发释放)// abandon newisa to undo the decrementnewisa = 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.(从侧表借位(最多借 RC_HALF))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 调整内联计数(借位后减1)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);//重新加载isa并重试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.(跳转到释放路径)}}
deallocate:// Really deallocate.ASSERT(newisa.isDeallocating());ASSERT(isa().isDeallocating());if (slowpath(sideTableLocked)) sidetable_unlock();//解锁侧表__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);//内存屏障,确保操作前的操作可见if (performDealloc) {this->performDealloc();//执行对象的dealloc方法}return true;//释放成功
#endif // ISA_HAS_INLINE_RC
}
由此可见, release方法的底层工作流程以==原子性减少对象引用计数为核心。首先通过 isTaggedPointer()
检查对象是否为小对象(如 int、小 NSNumber等),若是则直接跳过引用计数管理;接着检查类是否覆盖自定义 release逻辑(hasCustomRR),若有则调用自定义实现(如 Swift 的 swiftRelease 或通过 objc_msgSend
发送 release 消息);若为元类(isMetaClass)则直接返回(元类无实例指向,无需释放);对于普通对象,通过 CAS(Compare-And-Swap)原子操作(LoadExclusive/StoreReleaseExclusive)安全减少内联引用计数(存储于 isa位域的 extra_rc字段);若内联计数溢出(extra_rc 减至负数),则从侧表(SideTable)借位(最多借 RC_HALF)并调整内联计数;最终若引用计数归零且无法借位,标记对象为释放状态(isDeallocating
),执行内存屏障确保可见性后调用 dealloc
释放内存。
小结
retain/release是MRC模式下引用计数的操作入口,我们可以通过它们直接操作引用计数字段。虽然现在xcode启用ARC计数,十分方便,但我们还是有必要了解MRC下相关的手动引用计数管理,这有利于我们更好地掌握引用计数机制。