ios-散列表

1,696 阅读4分钟

ios 引用计数 retain release过程中不免要操作一张散列表(taggedPoint 不在此次讨论范围内)

那么散列表究竟是个什么样的结构,它与引用计数的关系是什么样的,这是我们此次通过源码探究的主题

进入libobjc 源码 (具体源码参考 - objc4-841.13可调试/编译源码更新(for M1))

以retain为例 涉及到一个结构

image.png

image.png

引出StripedMap

image.png

StripedMap 中包含一个数组,数组个数 -- arm 8; x86-64 64

以arm为例,就是 StripedMap中存储了8张表

image.png

我们具体操作的时候 是取StripedMap 中的某一张表

取表的过程

  • 通过一个key,也就是object的指针,也就是地址,可以认为是唯一的(具体虚拟地址偏移不在此次讨论范围),用过字典,应该知道这是什么意思

  • 把指针转换为 整型地址,也就是一个无符号整型变量 --- uint_var

  • 哈希函数 - (uint_var >> 4) ^ (unint_var >> 9) % 8(不做说明,后面的StripeCount 都按8计,也就是arm结构), 得到的是 数组中的索引index, 范围0 ~ 7

  • 通过 array[index] 取出 SideTable

由于 StripedMap 是全局的,必然存在访问问题,表中的元素 加锁 解锁操作

image.png

上面最开始的retain示例中,SideTables(), 就是获取到了 StripedMap

SideTables()[this] 就是 StripedMap[对象指针], 上面已提到过运算符[]重载

SideTable

image.png

你会发现操作这两表时 为了线程安全,避免不了锁的操作,必然存在性能消耗与效率问题

既然 表是全局的,必然存在回收机制

以refcnts为例, 拿到 SideTable之后,进一步 取RefcountMap

StripedMap[对象指针].refcnts ---> RefcountMap

image.png

其实 RefcountMap 为了安全考虑,掩盖了指针

继续向上溯源 objc::DenseMap

image.png

继续 DenseMapBase

image.png

image.png

又又出现了 运算符[]重载

  • StripedMap[对象指针].refcnts ---> RefcountMap

  • StripedMap[对象指针].refcnts[对象指针] ---> 引用计数存储的引用(简单理解就是获取到了 对象的引用计数变量)

    • 其实 并不是直接取出引用计数了
    • 还涉及一层,只是把这里的细节全展示,你会一个感觉,脑仁疼
    • 由于这块c++做了封装,所以不拿出来分析

理解散列表

对于散列表的理解,需要自带一些抽象气质

  • 全局StripdMap可以理解为三层套娃结构

  • 第一层,通过 对象指针地址 经过哈希函数运算,得到 StripdMap中数组结构的索引

  • 第二层,通过hash得到的索引,从 StripdMap中数组结构 中取出 Sidetable

  • 可以选择是使用SideTable 的引用计数表 还是 弱引用表

  • 引用计数表(SideTable成员refcnts)为例,refcnts[对象指针] --> Bucket 结构

    image.png

    • BucketT 是个 <key,value> 键值结构

    image.png

  • Bucket中 取出value

  • 还不算完,取出的value不是纯粹意义上整型变量,而应该看待为一个 bit结构,只有相应位置才存储引用计数,其余位有特殊功能

  • Bucket - value 是从低位 第2个bit位开始存储 引用计数的,也就是每次加减 都是 2 或者 是 1<<1 这样的操作

image.png

引用计数表 - 引用计数逻辑

引用计数的操作主要包含两部分 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结构(<key,value>结构)

      • Buckets(一个一个的Bucket)结构, 如果hash得出的索引位置 正好命中了key,就没必要再继续遍历查找了,如果hash没有命中,就只能按部就班挨个偏移位置查找key是否相等 当然取决于hash函数的设计了 冲突概率越少 hash函数越好
    • 散列表 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的缘故了)