iOS内存管理(Nonpointer_isa+散列表+retain+release)

698 阅读5分钟

前言

上篇文章关于内存管理系列iOS内存管理(Tagged Pointer技术),主要讲解了小对象的内存管理。这篇博客主要讲解关于对象的内存管理,主要涉及到Nonpointer_isa散列表.

在讲解retain、release之前我们需要先了解什么是Nonpointer_isa,以及散列表的一个结构

nonpointer_isa(非指针类型)

isa分为pointer_isa(指针类型)和非指针类型(Nonpointer_isa),间单的理解就是,如果isa是指针类型,那么就是一个纯的地址,没有做其他处理。如果是一个非指针类型,那么isa就是64位的地址,不止包含地址,还有其他的一些字段。

isa 数据结构

arm64.png

x86_64.png 其中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源码

截屏2021-09-22 下午1.36.05.png 截屏2021-09-22 下午1.37.15.png 截屏2021-09-22 下午1.37.28.png

  • 全局维护了一个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:弱引用表

用一张图来总结内存中散列表结构:

截屏2021-09-22 下午6.02.56.png

RefcountMap结构

继续查看RefcountMap源码

截屏2021-09-22 下午2.30.50.png 只看 RefcountMap 的本质是一个 DenseMap 类型,也是通过哈希运算的方式通过对象的指针获取表中的内容,并进行操作。

  • DisguisedPtr<objc_object> 伪装 objc_object 指针。
  • size_t 表示引用计数的值
  • RefcountMapValuePurgeable 一个结构体,只定义了一个静态内联函数 isPurgeable,入参为 0 时返回 true,否则返回 false

retain实现

我们知道ARC的环境下,系统会自动的帮我们调用retain,下面我们在objc查看源码。

retain->rootRetain

截屏2021-09-22 下午3.39.57.png 重点在rootRetain,主要看一下rootRetain实现,源代码就不直接复制了,主要讲一些关键的点。

小对象指针不参与引用计数的处理 截屏2021-09-22 下午3.42.07.png

引用计数的核心代码在do while循环里面:

截屏2021-09-22 下午4.00.30.png

  • 如果是没有优化的isa,直接通过sideTable存储引用计数

sidetable_retain源码实现 截屏2021-09-22 下午4.08.51.png 我们发现sidetable的源码里引用计数加的是SIDE_TABLE_RC_ONE,而不是1.因为在size_t结构中,前两位不是储存引用计数的,第一位存储的是是否有弱引用指针指向,第二位存储的是对象是否在被回收中。所以,在增加其引用计数时需要右移两位再进行增加,所以用到了这个系统的宏SIDE_TABLE_RC_ONE

截屏2021-09-22 下午4.14.29.png 通过源码里面定义的宏就可以看出来,如上图所示.

  • 如果真正销毁,引用计数不做处理

截屏2021-09-22 下午4.26.36.png

  • isa 里面的extra_rc++ 如果extra_c里面没有满的时候,extra_c++,其中carry表示是否已经加满

截屏2021-09-22 下午4.29.03.png

其中RC_ONE也是一个宏,上面也讲了extra_c存储在56-63位,所以RC_ONE要左移56位。才能找到extra_c所在的位置。

  • 如果extra_rc里面存满了,就会存储到sidetable里面

截屏2021-09-22 下午4.34.40.png 但是extra_rc,只存储了7位也就是127,剩下的会存储到sideTable里面.

为什么要这么操作呢? 主要是因为在进行散列表操作时进行了锁的操作,这样会影响性能,所以在extra_rc满状态下,会将其满状态的一半放到散列表中,避免频繁操作散列表。同时extra_rc满状态也不是频繁的出现slowpath(carry),所以满状态的一半已经有相当大的存储空间了!

  • sidetable_addExtraRC_nolock,散列表引用计数

截屏2021-09-22 下午4.53.12.png 如果引用计数满了也就是SIDE_TABLE_RC_PINNED,直接返回不处理。 否者调用:addc 进行+1操作。这边进行了一个左移动的操作,因为第二位才开始存储引用计数。

release 实现

查看objc源码 release->rootRelease,核心代码主要在rootRelease里面。和retain方法一样也会判断是否是小对象,是否真正销毁。

  • 调用subc,执行extra_rc--操作

截屏2021-09-22 下午5.12.46.png

  • 如果extra_rc没有了,sidetable引用计数执行减操作,这里的减操作可能不太一样

截屏2021-09-22 下午5.20.23.png

截屏2021-09-22 下午5.21.08.png 会先从sidetable转移RC_HALFextra_rc中。不断的循环操作,直到sidetable里面引用计数为0.

  • 清理完成之后会自动调用delloc方法

截屏2021-09-22 下午5.33.10.png

delloc流程

  • rootDealloc

截屏2021-09-22 下午5.35.37.png

  • 如果没有弱引用、关联对象、c++构造函数、散列表没有存储、直接free。
  • 否则调用object_dispose->objc_destructInstance

截屏2021-09-22 下午5.38.35.png 如果有c++构造、关联属性移除

  • 继续调用clearDeallocating 如果有弱引用和散列表相关的则也移除掉

截屏2021-09-22 下午5.41.56.png

retainCount的获取

截屏2021-09-22 下午5.27.10.png 这个不用看源码其实我们也能够想的到 retainCount = extra_rc +sidetable_getExtraRC