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

相关推荐
坏小虎21 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年1 天前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技1 天前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally1 天前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14801 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally2 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手2 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero2 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
ii_best2 天前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器
用户223586218203 天前
WebKit WebPage API 的引入尝试与自研实现
ios