本文涉及到的CPU架构为arm64,其它架构大同小异。
源码来自苹果开源-runtime。
Objective-C中采用引用计数机制来管理内存,在MRC时代,需要我们手动retain
和release
,在苹果引入ARC后大部分时间我们不用再关心引用计数问题。但是为了深入Objective-C本质,引用计数究竟是怎么实现的还是值得我们去探寻的。
ISA
OC中的对象的实质其实是结构体,其中大部分对象都有isa,指向类对象(有一种神奇的存在叫做Tagged Pointer
),源码中关于对象结构体objc_object
定义如下:
1 | // objc-private.h |
Tagged Pointer
除了有一种特殊的对象Tagged Pointer
,这种类型的对象值就存在指针当中,存取性能高。可以用来存储少量数据的对象,例如NSNumber、NSDate、NSString。(更多Tagged Pointer知识,具体看这篇Tagged Pointer小记)。也就没有引用计数、内存释放的问题。
NONPOINTER ISA
arm64架构isa占64位,苹果为了优化性能,存储类对象地址只用了33位,剩下的位用来存储一些其它信息,比如本文讨论的引用计数。
NONPOINTER ISA存储的字段定义如下:
1 | # if __arm64__ |
extra_rc
那引用计数存在哪里呢?秘密就在extra_rc
中。
extra_rc只是存储了额外的引用计数,实际的引用计数计算公式:
引用计数=extra_rc+1
。
extra_rc
占了19位,可以存储的最大引用计数:$2^{19}-1+1=524288$,超过它就需要进位到SideTables
。SideTables是一个Hash表,根据对象地址可以找到对应的SideTable
,SideTable
内包含一个RefcountMap
,根据对象地址取出其引用计数,类型是size_t
。
它是一个unsigned long
,最低两位是标志位,剩下的62位用来存储引用计数。我们可以计算出引用计数的理论最大值:$2^{62+19}=2.417851639229258e24$。
其实isa能存储的524288在日常开发已经完全够用了,为什么还要搞个Side Table?我猜测是因为历史问题,以前cpu是32位的,isa中能存储的引用计数就只有$2^{7}=128$。因此在arm64下,引用计数通常是存储在isa中的。
retain
有了前面的铺垫,我们知道引用计数怎么存储的了,那引用计数又是怎么改变的呢?通过剖析retain
源码我们就可以得出结论了。
objc_object的方法全部定义在objc-object.h文件中,全是内联函数,应该是为了性能的考虑。
我们来看看retain
的函数定义
1 | inline id |
这层比较简单,做了三件事情:
- 判断指针是不是
Tagged Pointer
- 判断是否有自定义
retain
,如果有调用自定义的。 - 最后调用
rootRetain
我们来看看关键函数rootRetain
的实现(为了便于阅读,代码有所删减)
1 | ALWAYS_INLINE id |
有一个细节可以了解下,如何用汇编来实现原子性操作。
1 | static ALWAYS_INLINE |
release
release
代码逻辑基本上就是retain
反过来走一遍,有点不同的是在引用计数减到0时,会调用对象的dealloc方法。
1 | ALWAYS_INLINE bool |
小结
引用计数存在哪?
Tagged Pointer
不需要引用计数NONPOINTER ISA
(isa的第一位为1)的引用计数优先存在isa中,大于524288了进位到Side Tables
- 非
NONPOINTER ISA
引用计数存在Side Tables
retain/release的实质
- 找到引用计数存储区域,然后+1/-1
- 如果是
NONPOINTER ISA
,还要处理进位/借位的情况 - release在引用计数减为0时,调用
dealloc
参考