iOS 中的引用计数

iOS 中的引用计数

最近面试,遇到引用计数的问题

并且由引用计数引出来的关联问题,在这说明一下

1、引用计数是什么

通常情况下,某一块地址有多少个指针指向了它,那么这个多少就是引用计数的值。是iOS 使用自动引用计数来管理内存。

2、引用计数的存储

总的来说,引用计数的存储位置可以分为三种情况,其目标是在保证正确性的前提下,最大限度地优化性能和内存使用

引用计数(retain count)的存储位置取决于对象所处的状态,主要有以下三种方式:

**1. ​存储在对象的 isa指针的额外比特位中(优化情况)​​ ​

存储在 isa指针中(非指针型 isa/ Tagged Pointer)

这是苹果进行的最重要的优化之一,目的是为了在多数常见情况下,不产生额外的存储开销。

a) 非指针型 isa

在 64 位系统后,一个指针地址是 64 位(8字节),但实际寻址并不需要全部 64 位。苹果利用了这些多余的比特位来存储信息,包括引用计数。

​原理​:对象的 isa指针不再直接指向类对象的内存地址,而是一个包含了类对象地址和对象状态信息的位域。

​存储内容​:isa结构中的 extra_rc字段(例如 19 个比特位)用于存储额外的引用计数。

当一个对象的引用计数为 1 时(即刚创建时),extra_rc的值为 0。

当有新的强引用持有该对象时,retain操作会尝试先给 extra_rc加 1。

只要 extra_rc没有溢出(即引用计数不太大),所有的引用计数操作都直接在这个 isa指针内完成,速度极快,且没有额外的内存访问。

b) Tagged Pointer(特殊情况)

对于某些小对象(如短字符串 NSString、小数字 NSNumber等),苹果使用了 Tagged Pointer 技术。此时,对象的值直接存储在其指针值中,​它根本不是堆上的一个真正对象。

​特点​:对于 Tagged Pointer,​不存在引用计数的概念。retain和 release操作都是空操作,因为它的"内存管理"就是简单的指针赋值和销毁,效率极高。

2. 存储在对象的 Side Table中(溢出情况)​​

当存储在 isa中的引用计数不够用时,系统会使用 Side Table。

​何时触发​:当对象的引用计数持续增加,导致 isa中的 extra_rc字段被塞满(溢出)时。

​工作原理​:

​一半一半策略​:当 extra_rc快满时,retain操作会将 extra_rc的大约一半值转移到一个全局的 SideTables中。

SideTables是一个哈希表,根据对象的地址可以找到对应的 Side Table。

每个 Side Table中有一个 RefcountMap(引用计数表),它以对象地址为 key,存储其额外的引用计数。

此时,isa中的 extra_rc会保留剩余的一半计数值,并设置一个标志位 has_sidetable_rc为 1,表示此对象有部分引用计数存储在 Side Table中。

​操作​:后续的 retain/release操作会先尝试修改 isa.extra_rc,如果不够,再去操作 Side Table中的值。

​为什么这样设计?​​

这是一种缓存思想。将最常用的、较小的引用计数放在访问速度最快的 isa指针中(相当于 L1 缓存),将不常用的、较大的计数部分放在访问稍慢的 Side Table中(相当于内存)。这保证了在绝大多数情况下(对象的引用数不多)性能最优。

3.两者结合使用(现代运行时的主流方式)

对于一个普通的 Objective-C 对象,其引用计数的存储是分级和混合的:

​创建时​:引用计数为 1,存储在 isa.extra_rc中(值为 0,表示实际计数是 extra_rc + 1)。

​频繁引用时​:retain操作优先增加 isa.extra_rc。

​计数溢出时​:将 isa.extra_rc的一部分转移到 Side Table中,isa只保留一部分。后续操作会同时检查两者。

​释放时​:release操作先减少 isa.extra_rc,如果它为 0 且 Side Table中有值,则再从 Side Table中借一些计数填回 isa.extra_rc。当所有计数归零时,对象被销毁。

3.为什么对象的isa.extra_rc 中会溢出,该怎么理解这种溢出

假设 isa.extra_rc字段的比特位数为 ​8​ 位。这意味着它能存储的最大无符号整数值是 2^8 - 1 = ​255。

根据苹果的优化策略,当 extra_rc快满时(比如达到 255 的一半,即 127 左右),系统会进行"分半"处理,而不是等到完全溢出(255)才处理,以防止溢出错误。

场景:一个被大量强引用的单例对象或管理器对象

假设我们有一个 NetworkManager的单例对象,它在 App 启动时被创建。然后,很多个网络请求模块(比如 200 个 RequestHandler对象)都需要强引用这个管理器来发送请求。

第一步:对象创建

​操作​:NetworkManager *manager = [[NetworkManager alloc] init];

​引用计数​:1

​存储方式​:因为是初始状态,引用计数为 1。在优化实现中,isa.extra_rc的实际值被设为 ​0,因为真正的引用计数是 isa.extra_rc + 1。这样设计可以多存一个计数。

isa.extra_rc= 0

isa.has_sidetable_rc= 0 (false,表示未使用 Side Table)

第二步:前 127 次 retain(假设没有 release)

​操作​:200 个 RequestHandler对象开始创建,并强引用 manager。我们执行了 127 次 retain操作。

​引用计数变化​:从 1 增加到 1 + 127 = ​128​

​存储方式​:所有的 retain操作都只是简单地增加 isa.extra_rc的值。

isa.extra_rc= 127 (因为实际计数是 127 + 1 = 128)

isa.has_sidetable_rc= 0

​此时状态​:引用计数完全存储在 isa指针中,速度极快。

第三步:第 128 次 retain- 触发溢出处理

这是最关键的一步。当系统发现 extra_rc的值已经比较大(比如达到了阈值 127,即 255 的一半),为了给后续的 retain留出空间,它会主动进行"分半"处理,将一部分计数转移到 Side Table。

​操作​:第 128 个 RequestHandler强引用 manager,触发第 128 次 retain。

​处理流程​:

a. ​准备转移​:系统决定将 isa.extra_rc中的大约一半(比如 128 的一半,64)转移出去。

b. ​操作 Side Table​:

在全局的 SideTables中,根据 manager对象的内存地址找到对应的 Side Table和它的 RefcountMap。

在 RefcountMap中为 manager创建一个条目,并将其引用计数值设置为 ​64。

c. ​更新 isa​:

将 isa.extra_rc的值更新为 128 - 64 - 1 = 63。(解释:原来的 128 次计数,减去移出去的 64,再减去对象本身占用的 1,剩下 63 存在 extra_rc中)。

将 isa.has_sidetable_rc标志位设置为 ​1,告诉运行时:"这个对象的部分引用计数在 Side Table 里,以后操作要注意。"

​最终存储状态(第 128 次 retain 后)​​:

​总引用计数​ = 1 (对象本身) + isa.extra_rc(63) + Side Table(64) = ​128。结果正确。

isa.extra_rc= 63

isa.has_sidetable_rc= 1

Side Table RefcountMap中 key(manager)对应的 value= 64

第四步:后续的 retain操作(第 129 次到第 200 次)

现在对象处于混合存储模式。

​操作​:继续创建 RequestHandler,执行第 129 次到第 200 次 retain(共 72 次)。

​处理流程​:每次 retain,系统会优先尝试增加 isa.extra_rc。

假设 isa.extra_rc从 63 开始增加,它最多能增加到 255。所以这 72 次 retain可以完全由 isa.extra_rc吸收。

​最终存储状态(第 200 次 retain 后)​​:

isa.extra_rc= 63 + 72 = ​135​

isa.has_sidetable_rc= 1

Side Table中的值保持不变,仍然是 ​64​

​总引用计数​ = 1 + 135 + 64 = ​200。结果正确。

相反的过程:release

当 RequestHandler们开始释放时:

前 135 次 release会先减少 isa.extra_rc,从 135 减到 0。这个过程很快。

当 isa.extra_rc减为 0,但 has_sidetable_rc为 1 时,系统知道 Side Table 里还有计数。

随后的 release操作会从 Side Table 中"借回"一部分计数到 isa.extra_rc中,然后再减少它。例如,系统可能会从 Side Table 的 64 中转移 50 到 isa.extra_rc,然后开始减少这 50。

如此循环,直到 Side Table 和 isa.extra_rc中的计数都归零,对象被正确销毁。

相关推荐
游戏开发爱好者89 小时前
iOS 崩溃日志分析工具全指南,多工具协同构建稳定性分析体系
android·macos·ios·小程序·uni-app·cocoa·iphone
00后程序员张20 小时前
如何提高 IPA 安全性 多工具组合打造可复用的 iOS 加固与反编译防护体系(IPA 安全 iOS 加固 无源码混淆 Ipa Guard 实战)
android·安全·ios·小程序·uni-app·iphone·webview
洞窝技术1 天前
前端开发APP之跨平台开发(ReactNative0.74.5)
android·react native·ios
EricStone1 天前
iOS语音转换SDK相关记录
ios
2501_916007471 天前
Fastlane 结合 开心上架 命令行版本实现跨平台上传发布 iOS App
android·ios·小程序·https·uni-app·iphone·webview
00后程序员张1 天前
iOS 26 内存占用监控 多工具协同下的性能稳定性分析实战
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915909061 天前
iOS 26 性能监控工具有哪些?多工具协同打造全方位性能分析体系
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_938773991 天前
Objective-C 类的归档与解档:NSCoding 协议实现对象持久化存储
开发语言·ios·objective-c
美狐美颜SDK开放平台1 天前
美颜SDK跨平台适配实战解析:让AI美颜功能在iOS与Android都丝滑运行
android·人工智能·ios·美颜sdk·直播美颜sdk·第三方美颜sdk·美颜api