ios 引用计数 retain release过程中不免要操作一张散列表(taggedPoint 不在此次讨论范围内)
那么散列表究竟是个什么样的结构,它与引用计数的关系是什么样的,这是我们此次通过源码探究的主题
进入libobjc 源码 (具体源码参考 - objc4-841.13可调试/编译源码更新(for M1))
以retain为例 涉及到一个结构
StripedMap 中包含一个数组,数组个数 – arm 8; x86-64 64
以arm为例,就是 StripedMap中存储了8张表
我们具体操作的时候 是取StripedMap 中的某一张表
取表的过程
通过一个key,也就是object的指针,也就是地址,可以认为是唯一的(具体虚拟地址偏移不在此次讨论范围),用过字典,应该知道这是什么意思
把指针转换为 整型地址,也就是一个无符号整型变量 — uint_var
哈希函数 - (uint_var >> 4) ^ (unint_var >> 9) % 8(不做说明,后面的StripeCount 都按8计,也就是arm结构), 得到的是 数组中的索引index, 范围0 ~ 7
通过 array[index] 取出 SideTable
由于 StripedMap 是全局的,必然存在访问问题,表中的元素 加锁 解锁操作
上面最开始的retain示例中,SideTables(), 就是获取到了 StripedMap
SideTables()[this] 就是 StripedMap[对象指针], 上面已提到过运算符[]重载
你会发现操作这两表时 为了线程安全,避免不了锁的操作,必然存在性能消耗与效率问题
既然 表是全局的,必然存在回收机制
以refcnts为例, 拿到 SideTable之后,进一步 取RefcountMap
StripedMap[对象指针].refcnts —> RefcountMap
其实 RefcountMap 为了安全考虑,掩盖了指针
继续向上溯源 objc::DenseMap
继续 DenseMapBase
又又出现了 运算符[]重载
StripedMap[对象指针].refcnts —> RefcountMap
StripedMap[对象指针].refcnts[对象指针] —> 引用计数存储的引用(简单理解就是获取到了 对象的引用计数变量)
对于散列表的理解,需要自带一些抽象气质
全局StripdMap可以理解为三层套娃结构
第一层,通过 对象指针地址 经过哈希函数运算,得到 StripdMap中数组结构的索引
第二层,通过hash得到的索引,从 StripdMap中数组结构 中取出 Sidetable
可以选择是使用SideTable 的引用计数表 还是 弱引用表
引用计数表(SideTable成员refcnts)为例,refcnts[对象指针] --> Bucket 结构
Bucket中 取出value
还不算完,取出的value不是纯粹意义上整型变量,而应该看待为一个 bit结构,只有相应位置才存储引用计数,其余位有特殊功能
Bucket - value 是从低位 第2个bit位开始存储 引用计数的,也就是每次加减 都是 2 或者 是 1<<1 这样的操作
引用计数的操作主要包含两部分 retain / release
retain
taggedPoint 没有引用计数操作, 非taggedPoint 也就是nonpoint_isa 与 纯isa存在引用计数操作
未开启指针优化的话,就是纯isa,只操作散列表中的引用计数
nonpoint_isa 操作isa 中的 extra_rc 与 散列表,散列表通过 isa中的位 has_sidetable_rc 来标识
以下主要说明的是 nonpoint_isa的操作过程
isa中取出 extra_rc, 如果+1 发现 19位 存满了,就extra_rc 折半,留一半,另一半存储到 散列表中,同时 has_sidetable_rc 标识为1
散列表的操作
StripedMap 对象指针 hash运算,从 StripedMap.array中取出 SideTable
SideTable.refcnts(引用计数表) 再次通过 对象指针 另一个hash函数运算,取出Bucket结构(
散列表 Bucket.value 低位开始 第二个位置 + 1,也就是 + 2,存储
当然散列表由于是全局,所以操作需要 加锁 开锁
release 与retain相反的过程
此处讨论的还是 nonpoint_isa 的全流程
首先 isa :: extra_rc 减 1,如果此时计数为0,同时 has_sidetable_rc 为0,没有引用散列表存储计数,就发送 dealloc
如果 extra_rc 减1之后 不为0 就没有其他操作
如果 extra_rc 减1为0, 同时 has_sidetable_rc 为1,就 从散列表 借出 extra_rc 所能存储的总数的一半,剩下的部分继续存储在散列表里, 取出的部分存到 extra_rc 里, 具体散列表的操作跟 retain过程中的一样,此处就不重复了
如果 extra_rc 从散列表 借完计数之后,散列表计数为0,就删除散列表中的bucket结点, has_sidetable_rc 置为0
下次release 继续 从 extra_rc 减1,直到减为0, 同时 has_sidetable_rc为0
发送 dealloc消息 (这就是为什么dealloc 不能调用父类dealloc的缘故了)