前言
上篇文章关于内存管理系列iOS内存管理(Tagged Pointer技术),主要讲解了小对象的内存管理。这篇博客主要讲解关于对象的内存管理,主要涉及到Nonpointer_isa
和散列表
.
在讲解retain、release之前我们需要先了解什么是Nonpointer_isa
,以及散列表
的一个结构
nonpointer_isa(非指针类型)
isa分为pointer_isa
(指针类型)和非指针类型(Nonpointer_isa
),间单的理解就是,如果isa
是指针类型,那么就是一个纯的地址,没有做其他处理。如果是一个非指针类型,那么isa
就是64位的地址,不止包含地址,还有其他的一些字段。
isa 数据结构
其中arm64和x86_64有些字段占用长度,或者位置可能不一样。
nonpointer
:是否开启指针优化,0未开启,1开启。has_assoc
:是否有关联对象has_cxx_dtor
:对象是否含有 C++ 或者 Objc 的析构器shiftcls
:类的指针magic
:对象是否完成初始化weakly_referenced
:是否为弱引用的对象deallocating
:对象是否正在执行析构函数(是否在释放内存)has_sidetable_rc
:判断是否使用散列表去存储引用计数extra_rc
:引用计数的值减1
散列表
散列表(Hash table,也叫哈希表) ,是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表
查看objc源码
- 全局维护了一个StripedMap变量,其内部实现是一个静态数组.在真机的数组元素最大个数为8,在模拟器上数组元素的最大个数64
- 通过将被引用对象的地址做
indexForPointer
运算使得每个被引用对象运算之后的结果在【0,stripeCount】. - 存储的值是抽象的PaddedT,在内存管理存储的结构体
SideTable
的实例.
SideTable结构
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
...
}
SideTable
是一个结构体主要用于辅助管理对象的引用计数和弱引用依赖.
- slock:线程安全,保证操作通一个表的时候线程安全
- refcnts:存储引用计数,如果
Nonpointer_isa
里面的extra_rc
存满了,就存储在refcnts
里面 weak_table
:弱引用表
用一张图来总结内存中散列表结构:
RefcountMap结构
继续查看RefcountMap
源码
只看
RefcountMap
的本质是一个 DenseMap 类型,也是通过哈希运算的方式通过对象的指针获取表中的内容,并进行操作。
DisguisedPtr<objc_object>
伪装objc_object
指针。size_t
表示引用计数的值RefcountMapValuePurgeable
一个结构体,只定义了一个静态内联函数isPurgeable
,入参为0
时返回true
,否则返回false
retain实现
我们知道ARC的环境下,系统会自动的帮我们调用retain,下面我们在objc
查看源码。
retain->rootRetain
重点在rootRetain,主要看一下rootRetain实现,源代码就不直接复制了,主要讲一些关键的点。
小对象指针不参与引用计数的处理
引用计数的核心代码在do while
循环里面:
- 如果是没有优化的
isa
,直接通过sideTable
存储引用计数
sidetable_retain源码实现
我们发现sidetable的源码里引用计数加的是
SIDE_TABLE_RC_ONE
,而不是1.因为在size_t
结构中,前两位不是储存引用计数的,第一位存储的是是否有弱引用指针指向,第二位存储的是对象是否在被回收中。所以,在增加其引用计数时需要右移两位再进行增加,所以用到了这个系统的宏SIDE_TABLE_RC_ONE
。
通过源码里面定义的宏就可以看出来,如上图所示.
- 如果真正销毁,引用计数不做处理
- isa 里面的extra_rc++ 如果extra_c里面没有满的时候,extra_c++,其中carry表示是否已经加满
其中RC_ONE
也是一个宏,上面也讲了extra_c
存储在56-63位
,所以RC_ONE
要左移56位。才能找到extra_c
所在的位置。
- 如果extra_rc里面存满了,就会存储到sidetable里面
但是extra_rc,只存储了7位也就是127,剩下的会存储到
sideTable
里面.
为什么要这么操作呢?
主要是因为在进行散列表操作时进行了锁的操作,这样会影响性能,所以在extra_rc
满状态下,会将其满状态的一半放到散列表中,避免频繁操作散列表。同时extra_rc
满状态也不是频繁的出现slowpath(carry)
,所以满状态的一半已经有相当大的存储空间了!
- sidetable_addExtraRC_nolock,散列表引用计数
如果引用计数满了也就是
SIDE_TABLE_RC_PINNED
,直接返回不处理。
否者调用:addc
进行+1操作。这边进行了一个左移动的操作,因为第二位才开始存储引用计数。
release 实现
查看objc源码 release->rootRelease,核心代码主要在rootRelease里面。和retain方法一样也会判断是否是小对象,是否真正销毁。
- 调用
subc
,执行extra_rc--
操作
- 如果extra_rc没有了,sidetable引用计数执行减操作,这里的减操作可能不太一样
会先从
sidetable
转移RC_HALF
到extra_rc
中。不断的循环操作,直到sidetable里面引用计数为0.
- 清理完成之后会自动调用delloc方法
delloc流程
- rootDealloc
- 如果没有弱引用、关联对象、c++构造函数、散列表没有存储、直接free。
- 否则调用
object_dispose->objc_destructInstance
如果有c++构造、关联属性移除
- 继续调用
clearDeallocating
如果有弱引用和散列表相关的则也移除掉
retainCount的获取
这个不用看源码其实我们也能够想的到
retainCount = extra_rc +sidetable_getExtraRC