swfit进阶-06-内存管理

227 阅读8分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

  • 本文主要介绍swift中引用计数的变化,来进行内存管理。

  • swift中使用自动引用计数(ARC)机制来追踪管理内存。 我们初始化一个Person,之后赋值给t,引用计数+1,之后再赋值给t1,引用计数+1,最后再赋值给t2,引用计数+1.我们打印指针指向的内存分布image.png

我们知道swift的实例对象本质是HeapObject结构体,是由metadatarefcounts组成。上面我画圈的就是它的引用计数的变化。

1. 引用计数的分析

  • 源码分析 查看HeapObjectrefcounts是宏定义的InlineRefCounts类型

image.png

查看InlineRefCounts的定义

image.png

查看RefCounts是一个模版类,接受的参数是RefCountBits

image.png

其中BitsType bits这个成员变量,后面的操作都是关于这个变量进行操作

image.png

  • 初始化的引用计数

我们上面打印的时候赋值给t的时候是0x3,初始化的时候引用计数是多少

  • 初始化 image.png

  • 引用计数

image.png

  • Initialized_t 默认初始化的引用计数为1

image.png

传入的对象为RefCountBits,实际上进行位域运算,根据定义的offset

image.png

如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount 传 0,unownedCount 传 1。那么 Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1,

  • 其中RefCountBits的分布如下 flutter渲染.jpg

所以我们最终计算的结果是

0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;

2. 强引用

我们在给初始化的对象进行强引用会使引用计数+1,文章开始的时候截图所示。那么怎么变化的呢?

我们编译成sil文件

image.png

开始p定义为全局地址,之后初始化成功后store到p,之后赋值是copy_addr操作。 SIL官方文档中关于copy_addr的解释相当于strong_retain

  • 其中的strong_retain对应的就是 swift_retain,其内部是一个宏定义,内部是_swift_retain_,其实现是对object的引用计数作+1操作

image.png

  • increment

image.pngInlineRefCounts进入,其中是c++中的模板定义,是为了更好的抽象,在其中查找bits(即decrementStrongExtraRefCount方法)

image.png 例如以prefCounts为例(其中33-62位是strongCount,每次增加强引用计数增加都是在33-62位上增加的,固定的增量为1左移33位,即0x200000000

  • 只有p时的refCounts0x0000000200000003
  • p + p1时的refCounts0x0000000400000003 = 0x0000000200000003 + 0x200000000
  • p + p1 + p2 时的refCounts0x0000000600000003 =0x0000000400000003 + 0x200000000
  • 针对上面的例子,可以通过CFGetRetainCOunt获取引用计数,发现依次是 2、3、4,默认多了一个1

image.png 当我们使用p打印的时候也会调用retain,导致引用计数+1 image.png

3. 弱引用分析

我们看一个循环引用的案例

class KBTeacher{

    var age: Int = 18

    var name: String = "KK"

    var subject: KBSubject?

    deinit{

        print("ee")

    }

}

class KBSubject{

    var subjectName: String

    var subjectTeacher: KBTeacher

    init(_ subjectName: String, _ subjectTeacher: KBTeacher) {

        self.subjectName = subjectName

        self.subjectTeacher = subjectTeacher

    }

    deinit{

        print("hh")

    }

}

var t = KBTeacher()


var subject = KBSubject.init("Swift ", t)

t.subject = subject

print("end")

我们打印结果没有走析构函数,说明了相互持有,无法释放,我们打破循环有2种方式,一种是弱引用,一种是无主引用

3.1 弱引用的概念

弱引用不会对其引用的实例保持强引用,因此不会阻止ARC释放被引用实例对象。这个特性可以打破循环引用的问题,我们在申明属性或者变量的时候,在前面加上weak这个关键词表示这是一个弱引用

image.png 这里注意的是在不在闭包里的话,声明的变量是全局变量,无法释放的。

由于弱引用不会保持对实例的引用,所以说实例被释放了弱引用仍然引用这个实例对象也是有可能的,ARC会在被引用的实例释放的时候好自动设置弱引用对象为nilweak是可选的optional,向空对象调用不会报错。

image.png

可以检查弱引用的值是否存在,可以像其他可选值一样,永远不会遇到野指针

3.2 弱引用探究

我们在弱引用处断点

image.png

查看swift_weakInit在源码的定义返回的是WeakReference的弱引用对象

image.png

查看nativeInit主要是获取sideTable之后进行存储。

image.png

  • formWeakReference创建sideTable,成功后插入弱引用对象。

image.png

  • allocateSideTable

image.png

  • 将创建的sideTable地址给InlineRefCountBits,并查看其初始化方法

image.png

根据sideTable地址 放到64位位域中,作了偏移操作并存储到内存,相当于将sideTable直接存储到了64位的变量中 image.png

2个标识位一个UseSlowRCShift:62,一个SideTableMarkShift:63 RefCountBitsT本质一个64位指针,把side存储到64位位域中,并做了一些标记。

  • 查看HeapObjectSideTableEntry 存放的是原有的HeapObejct对象和引用计数 image.png

  • SideTableRefCountBits 存放的是弱引用计数

image.png

上面根据得到的散列表会左移3位

image.png

SideTableUnusedLowBits宏定义 image.png

因此我们打印我们之前的对象地址位0x00000001005053e0打印它的内存分布,在计算器上显示refcounts的地址

image.png

还原散列表的话,高位进行抹0

image.png

之后右移3位进行还原。

image.png
验证打印的结果,和我们数据类型相同。 image.png

3.3 小结

可以看到官方关于引用计数的描述分为2种情况

image.png 它们的引用计数:

  1. 无弱引用:strong RC + unowned RC
  2. 有弱引用:strong RC + unowned RC + weak RC

4. 无主引用

无主引用和弱引用类似,都是不持有引用对象的,但是无主引用必须保证有值,否则会造成野指针崩溃

image.png

根据苹果的官方文档说明:当2个对象的生命周期不一致的时候必须使用weak进行修饰打破循环,但是如果他们的生命周期一致的话可以使用unowned

image.png

老师和课程是相互持有的,老师不在课堂课程就没法上了。有在上课的话一定老师,因此他们的生命周期是一致的,可以使用unowned进行修饰。

5. 闭包循环引用

我们在oc中知道block使用不当会造成循环引用问题,那么swift的闭包是否会有同样问题
先看下这个闭包 image.png 这里闭包捕获了外部的变量,打印的结果也是修改后的结果。

  • 换成class对象的话

image.png 捕获了对象,并修改了对象的age的值但是没有执行deInit,此时t是全局变量没有释放.

image.png

5.1 闭包循环引用的解决

此时clourse没有和我们实例对象相互引用所以可以释放,当相互引用时

image.png 此时没有打印dealloc,说明没有释放,闭包强持有了对象。

image.png

引用计数发生了变化,强持有了。怎么解决呢

使用weak image.png

也可以使用unowned

image.png

5.2 闭包捕获列表

  • [weak t] / [unowned t] 在swift中被称为捕获列表 捕获列表:

  • 默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。您可以使⽤捕获列表来显式控制如何在闭包中捕获值。

  • 在参数列表之前,捕获列表被写为⽤逗号括起来的表达式列表,并⽤⽅括号括起来。如果使⽤捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使⽤ in 关键字。

image.png

我们可以发现闭包中打打印的age变量时白色的,相当于我们捕获的age 进行了值拷贝,而height是外部变量相当于指针拷贝,所以我们外部修改也会造成打印的改变。

6.总结

  • swift中对象创建的时候引用计数就会+1无弱引用的时候我们是对heapObject中的refcounts进行操作,它是一个64位指针信息,包含了强引用计数无主引用计数和一些flags

  • 弱引用是对sideTable进行操作,第一次的时候会创建这个弱引用表,把它放到我们refcounts指针的位置,并对它进行偏移操作。是由实例对象Object+ 强引用+无主引用+弱引用计数组成

  • 对于swift中的循环引用可以使用weak或者unowned进行修饰打破循环,同一生命周期可以使用unowned,否则使用weak,防止野指针。

  • swift中闭包的捕获列表值拷贝