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中的计数都归零,对象被正确销毁。

相关推荐
2501_9160074710 小时前
iOS 压力测试的工程化体系,构建高强度、多维度、跨工具协同的真实负载测试流程
android·ios·小程序·uni-app·cocoa·压力测试·iphone
2501_9160088912 小时前
API接口调试全攻略 Fiddler抓包工具、HTTPS配置与代理设置实战指南
前端·ios·小程序·https·fiddler·uni-app·webview
2501_9159214313 小时前
iOS 开发者工具推荐,构建从调试到性能优化的多维度生产力工具链(2025 深度工程向)
android·ios·性能优化·小程序·uni-app·iphone·webview
00后程序员张15 小时前
全面解析网络抓包工具使用:Wireshark和TCPDUMP教程
网络·ios·小程序·uni-app·wireshark·iphone·tcpdump
游戏开发爱好者815 小时前
Mac 抓包软件怎么选?从 HTTPS 调试、TCP 数据流分析到多工具协同的完整抓包方案
tcp/ip·macos·ios·小程序·https·uni-app·iphone
马拉萨的春天17 小时前
iOS中广告SDK如何判断一个广告是否真实展示
macos·ios·cocoa
ajassi200018 小时前
开源 Objective-C IOS 应用开发(十九)视频的播放
ios·开源·objective-c
2501_9159184118 小时前
苹果上架 iOS 应用的工程实践,一次从零到上线的完整记录
android·ios·小程序·https·uni-app·iphone·webview
ajassi200019 小时前
开源 Objective-C IOS 应用开发(二十二)自定义控件--车速仪表盘
ios·开源·objective-c
從南走到北19 小时前
JAVA国际版同城跑腿源码快递代取帮买帮送同城服务源码支持Android+IOS+H5
android·java·ios·微信小程序