当前位置: 首页 > news >正文

【iOS】retain/release底层实现原理

前言

  在前面OC的学习中,我们了解到了OC中的关键字,今天我们来具体解析一下strong、copy、retain、release的实现原理,他们都是内存管理的核心机制,其底层实现深度依赖引用计数(Retain Count)运行时(Runtime)的 SideTable机制

前情知识

  在了解其底层原理之前,我们先来回顾一下引用计数的相关概念。

  OC内存管理的核心是引用计数(ARC 下由编译器自动管理,MRC 下手动操作)。每个对象有一个隐式的引用计数retainCount),表示当前有多少个“拥有者”持有该对象:

  • 引用计数 +1:对象被“持有”(如 retainstrong赋值)。
  • 引用计数 -1:对象被“释放”(如 releasestrong变量超出作用域)。
  • 引用计数为 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的工作流程以原子性增加对象引用计数为核心,代码执行的流程大致如下:

首先初始化 sideTableLockedtranscribeToSideTable 状态变量,并通过 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的底层逻辑:

  1. 检查对象是否为小对象,若是则直接返回。
  2. 获取 SideTable并减少引用计数。
  3. 若引用计数减至 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下相关的手动引用计数管理,这有利于我们更好地掌握引用计数机制。

http://www.lryc.cn/news/606614.html

相关文章:

  • CMake set_source_files_properties使用解析
  • 15. 若依框架的Security Config
  • 微服务消息队列之RabbitMQ,深入了解
  • Docker状况监控
  • 加密与安全
  • Idea集成Jenkins Control插件,在IDEA中触发Jenkins中项目的构建
  • LLM Prompt与开源模型资源(2)提示工程关键技术
  • GaussDB 数据库设计规范
  • JavaScript 高效入门指南:从基础到实战(VSCode 版)
  • 【03】海康MVS V4.3.0 ——安装教程、查看示例、库、头文件、开发指南
  • 应用app的服务器如何增加高并发
  • 解读LISA:通过大型语言模型实现推理分割
  • 【无标题】严谨推导第一代宇宙的创生机制,避免无限回溯问题。
  • alaxea机器人由星海图人工智能科技有限公司研发的高性能仿人形机器人
  • 渗透测试常用指令
  • SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:日志管理(四)集成Spring Security
  • 如何将消息转移到新 iPhone
  • 1688商品评论API接口逆向分析与数据采集
  • 视频质量检测中卡顿识别准确率↑32%:陌讯多模态评估框架实战解析
  • 2025年文生图模型stable diffusion v3.5 large的全维度深度解析
  • 嵌入式系统中常用通信协议
  • RAGFlow Agent 知识检索节点源码解析:从粗排到精排的完整流程
  • 电脑的时间同步电池坏掉了,每次开机都要调整时间
  • 江协科技STM32 11-4 SPI通信外设
  • 生物医药研究数据分析工具测评:衍因科技如何重塑科研范式?
  • Git Pull 时遇到 Apply 和 Abort 选项?详解它们的含义与应对策略
  • LeetCode 面试经典 150_数组/字符串_买卖股票的最佳时机(7_121_C++_简单)(贪心)
  • 《黑马笔记》 --- C++ 提高编程
  • 【数据结构入门】时间、空间复杂度的计算
  • 基于C++的protobuf协议使用(四)项目应用与总结