Swift和OC一样,也是采用了依据引证计数的ARC内存管理方案,在OC中ARC引证主要有强引证弱引证,Swift的ARC引证除了强引证弱引证外,又添加了一个无主引证

在第一篇文章Swift进阶(一) —— 类与结构体中,咱们知道Swift实质是一个HeapObject的结构体,而在HeapObject结构体中有两个成员变量:metadatarefCounts。其间metadata指向元数据目标,存储Swift数据结构类、结构体的基本信息、特点和办法列表等。而这个refCounts特点则是和ARC的引证计数相关。

refCounts的实质

咱们先从源码上面来查找refCounts详细做了什么。

首要咱们先从HeapObject.h去找refCounts特点

Swift进阶(五)—— 内存管理
咱们能够看到refCounts的类型是InlineRefCounts,在RefCount.h文件中,咱们找到了InlineRefCounts的界说。
Swift进阶(五)—— 内存管理
从界说中,咱们能够知道InlineRefCounts是一个模板类,是一个接收了InlineRefCountBits类型参数的RefCounts类。接着咱们去检查RefCounts类的数据结构
Swift进阶(五)—— 内存管理
RefCounts类中的API咱们能够看到,在这个类里边,都是在操作RefCountsBits这个传进来的泛型参数,咱们能够知道RefCounts类实质上是对当时引证计数的一个包装。因而,引证计数的类型取决于传进来的这个参数类型,也便是上面的InlineRefCountBits类。
Swift进阶(五)—— 内存管理
经过查找InlineRefCountBits的界说,咱们知道,这又是一个模板函数。咱们先看一下RefCountIsInline这个参数。
Swift进阶(五)—— 内存管理
能够看到这个参数是枚举类型,要么是true,要么是false。接下来咱们检查RefCountBitsT这个类。
Swift进阶(五)—— 内存管理
RefCountBitsT类中,只要bits这个成员变量,这个bitsRefCountBitsInt类型。因而咱们能够确定引证计数类型应该是RefCountBitsInt类型,咱们去看一下RefCountBitsInt类。
Swift进阶(五)—— 内存管理
能够看到,Type的类型是一个uint64_t的位域信息,在这个uint64_t的位域信息中存储着运行生命周期的相关引证计数

到了这儿,咱们仍然不知道是怎样设置的,咱们先来看⼀下,当咱们创立⼀个实例目标的时候,当时的引⽤计数是多少?

咱们先来检查HeapObject类的初始化办法

Swift进阶(五)—— 内存管理
Swift进阶(五)—— 内存管理
HeapObject类的初始化办法中,咱们看到了一个Initialized,这是对refCounts的初始化赋值。咱们查找一下这个Initialized,发现它是Initialized_t枚举类型。
Swift进阶(五)—— 内存管理
查找Initialized_t,咱们找到了refCounts的初始化办法
Swift进阶(五)—— 内存管理
经过上面的注释能够看出来,这儿是给一个新创立的目标的引证计数置为1。传入的参数为RefCountBits(0, 1),这儿的RefCountBits便是咱们上面说的RefCountBitsT类。咱们去检查RefCountBitsT类的初始化办法,代码如下:
Swift进阶(五)—— 内存管理
从上面的初始化办法咱们能够知道,strongExtraCount是0, unownedCount是1,初始化时对这两个数进行了偏移,接下来咱们来查找StrongExtraRefCountShiftPureSwiftDeallocBitCountPureSwiftDeallocShift这三个偏移数字是多少?

经过全局查找StrongExtraRefCountShift,咱们找到了这三个常量的界说:

Swift进阶(五)—— 内存管理
其间PureSwiftDeallocShift的值是0,PureSwiftDeallocBitCount的值是1。而StrongExtraRefCountShift则是经过shiftAfterField办法核算出来。咱们查找一下shiftAfterField办法的界说
Swift进阶(五)—— 内存管理
依据上面的办法界说,咱们能够求出StrongExtraRefCountShift的值,如下所示:

 static const size_t StrongExtraRefCountShift = shiftAfterField(IsDeiniting)
    = IsDeinitingShift  + IsDeinitingBitCount 
    = shiftAfterField(UnownedRefCount)  + 1 
    = UnownedRefCountShift +  UnownedRefCountBitCount +  1
    = shiftAfterField(PureSwiftDealloc)  + 31  + 1
    = PureSwiftDeallocShift  + PureSwiftDeallocBitCount +  32
    = 0  + 1  + 32
    = 33
 static const size_t UnownedRefCountShift = PureSwiftDeallocShift  + PureSwiftDeallocBitCount = 0 + 1 = 1

咱们能够知道,每一个目标创立后,refcounts的特点值如下所示。

refcount =    0 << 33 | 1 << 0 | 1 << 1 = 0x0000000000000003

终究咱们经过代码验证一下。

Swift进阶(五)—— 内存管理
经过lldb指令打印地址,咱们能够看到:
Swift进阶(五)—— 内存管理
打印出来的成果和咱们料想的一样。

强引证

在Swift中,默许状况下,对一个实例目标的引证是强引证。当咱们创立一个实例目标后,它的refcount特点会是0x0000000000000003,当咱们对这个实例目标进行强引证后,refcount特点会发生什么改变呢?

代码如下:

Swift进阶(五)—— 内存管理
然后分别在t1、t2、print(“end”)处分别设置断点,然后运用lldb指令x/8g检查t的内存结构,每次过一个断点打印内存结构一次。打印成果如下:
Swift进阶(五)—— 内存管理
咱们能够看到,当对实例变量t进行强引证的过程中,trefcount特点从0x0000000000000003-> 0x0000000200000003 ->0x0000000400000003。经过核算器对这几个地址进行解析,检查它们的改变。
Swift进阶(五)—— 内存管理
Swift进阶(五)—— 内存管理
经过核算器咱们能够看到,当引证次数添加时,refcount特点在高位进行位移操作。

咱们下面经过源码去剖析一下。首要经过汇编代码来检查一下强引证操作怎样完成:

Swift进阶(五)—— 内存管理
从汇编代码中能够看到,当对一个实例变量强引证时,调用了一个swift_retain办法。接下来咱们经过源码检查swift_retain怎样完成。
Swift进阶(五)—— 内存管理
经过检查swift_retain函数完成,咱们能够看到,refCounts.increment(1),咱们继续检查increment函数的完成。
Swift进阶(五)—— 内存管理
在这儿边咱们看到了incrementStrongExtraRefCount这个办法,从字面意思就能够看出,这个办法是用来添加强引证计数的。咱们深入到这个办法里边,检查怎样添加强引证计数。
Swift进阶(五)—— 内存管理
咱们能够看到,强引证计数的添加是先把要添加的引证计数参数往左移动33位,然后在和本来的引证计数相加,也便是说每做一次强引证,refcount特点就会加上0x200000000

Swift进阶(五)—— 内存管理
上面这张图是refcount特点64位域信息下面的存储方式,咱们能够看到在高位33-62位用于存储强引证计数。而在第32位isDeinitingMask用来标识这个实例是否能够析构,假如当一个实例不再被强引证,那么它就会被开释掉,咱们来验证一下。代码如下所示:
Swift进阶(五)—— 内存管理
经过lldb指令打印内存地址,显现如下:
Swift进阶(五)—— 内存管理
t = nil后,refcount地址显现为0x100000003,经过核算器进行解析后,咱们发现在32位上标识为1。也便是说这个实例能够被开释掉。
Swift进阶(五)—— 内存管理

循环引证

咱们在运用OC开发时,经常会遇到运用强引证呈现循环引证导致实例目标无法开释的问题,在Swift中,也会呈现这种状况,比如以下代码所示:

Swift进阶(五)—— 内存管理
t = nil时,不会触发deinit办法,因为这两个实例目标之间呈现了循环引证。

Swift提供了两种办法⽤来处理你在使⽤类的特点时所遇到的循环强引⽤问题:弱引⽤(weak reference)⽆主引⽤(unownedreference)

弱引证

弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ARC开释被引⽤的实例。这个特性阻⽌了引⽤变为循环强引⽤。声明特点或许变量时,在前⾯加上weak关键字标明这是⼀个弱引⽤。

因为弱引⽤不会强保持对实例的引⽤,所以说实例被开释了弱引⽤依旧引⽤着这个实例也是有或许的。因而,ARC 会在被引⽤的实例被开释是⾃动地设置弱引⽤为 nil 。因为弱引⽤需要允许它们的值为nil,它们⼀定得是可选类型。

咱们经过一个事例来看弱引证在内存的存储方式

Swift进阶(五)—— 内存管理
经过lldb指令检查存储特点
Swift进阶(五)—— 内存管理
能够看到,当对一个实例实施弱引证后,refcount地址发生了很大的改变,那这个改变是怎样来的?

首要咱们先去检查汇编代码:

Swift进阶(五)—— 内存管理
能够看到,当运用了弱引证后,汇编代码里边调用了swift_weakInit办法。咱们去源码里边检查这个办法。
Swift进阶(五)—— 内存管理
这个办法里边调用了nativeInit办法,咱们再去检查nativeInit办法

Swift进阶(五)—— 内存管理
这个代码里边主要是创立了一个side,然后把这个side存储起来。所以咱们继续检查formWeakReference这个办法。
Swift进阶(五)—— 内存管理
在这个办法里边,经过allocateSideTable创立一个散列表,然后回来一个HeapObjectSideTableEntry。 终究咱们检查一下allocateSideTable办法。
Swift进阶(五)—— 内存管理
咱们能够看一下这个函数是怎样创立散列表的:

1.首要获取原先的引证计数refcounts特点。

2.判别refcounts特点有没有散列表,假如有,则直接回来散列表。

3.假如没有散列表,则重新创立一个散列表,并以此创立一个新的refcounts特点。

4.对本来的散列表做一些析构处理。

接下来咱们来看一下HeapObjectSideTableEntry这个类,从源码里边去查找这个类,发现了苹果官方关于引证计数的一些注释:

Swift进阶(五)—— 内存管理
从注释里边咱们能够知道,当对实例目标强引证的时候,运用了InlineRefCounts,引证计数核算规则是 strong RC + unowned RC + flags,而对实例目标就行弱引证后,则变成了HeapObjectSideTableEntry,引证计数核算规则是strong RC + unowned RC + weak RC + flags

咱们来看一下HeapObjectSideTableEntry类:

Swift进阶(五)—— 内存管理
这个类里边存储了当时实例目标objectSideTableRefCounts类型的refcounts特点。咱们看一下 SideTableRefCounts是什么类。
Swift进阶(五)—— 内存管理
能够看到它也是RefCountBits的模板类。
Swift进阶(五)—— 内存管理
看了SideTableRefCounts的详细完成,咱们能够知道,它是继承了RefCountBitsT这个类,所以它除了有64位域信息外,还多了一个32位的weakBits特点,初始化的时候,weakBits为1,新增一个弱引证,weakBits加1。

在强引证剖析中,InlineRefCountBits终究经过RefCountBitsT这个类来完成,找到和HeapObjectSideTableEntry相关的初始化办法。

Swift进阶(五)—— 内存管理
经过检查源码,能够知道UseSlowRCShift为63,SideTableMarkShift为62,SideTableUnusedLowBits值为3。

所以当对一个实例变量进行弱引证后,refCounts存储方式是这样的:先创立一个散列表,同时把散列表的存储地址右移3位,再把高位63、62位地址置为1,终究把这个地址存储到refCounts特点中。

接下来,咱们开始对弱引证后的地址进行解析,得到散列表的地址。首要看一下代码

Swift进阶(五)—— 内存管理
接下来运用lldb指令获取refCounts的内存地址。
Swift进阶(五)—— 内存管理
把获取到的内存地址的高63位、62位置为0
Swift进阶(五)—— 内存管理
再往左移动3位,得到散列表的内存地址。
Swift进阶(五)—— 内存管理
运用lldb指令解析这个内存地址
Swift进阶(五)—— 内存管理
所以,当对一个实例目标进行弱引证的时候,实质上是建立了一个散列表。

无主引证

和弱引⽤相似,⽆主引⽤unowned不会牢牢保持住引⽤的实例。但是不像弱引⽤,总之,⽆主引⽤unowned假定是永远有值的。

依据苹果的官⽅⽂档的主张。当咱们知道两个目标的⽣命周期并不相关,那么咱们必须使⽤weak。相反,⾮强引⽤目标拥有和强引⽤目标同样或许更⻓的⽣命周期的话,则应该使⽤unowned

假如两个目标的⽣命周期完全和对⽅不要紧(其间⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤weak

假如你的代码能确保:其间⼀个目标毁掉,另⼀个目标也要跟着毁掉,这时候,能够(慎重)⽤unowned

Weak VS unowned

  • 假如两个目标的⽣命周期完全和对⽅不要紧(其间⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤ weak
  • 假如你的代码能确保:其间⼀个目标毁掉,另⼀个目标也要跟着毁掉,这时候,能够(慎重)⽤unowned

闭包的循环引证

在Swift中,创立一个闭包会⼀般默许捕获咱们外部的变量,如下代码所示:

Swift进阶(五)—— 内存管理

从打印成果能够看出来,闭包内部对变量的修正将会改变外部原始变量的值。这样咱们就会遇到一个问题,假如咱们在class的内部界说⼀个闭包,当时闭包拜访特点的过程中,就会对咱们当时的实例目标进⾏捕获:

Swift进阶(五)—— 内存管理
如上图所示,打印的成果没有deinit办法,也便是说明这个实例目标和闭包形成了循环引证,程序完毕后无法进行开释。

那咱们应该怎样处理循环引证呢?

1.运用闭包的捕获列表,在捕获列表中声明对引证的实例目标为weak引证。

Swift进阶(五)—— 内存管理
从打印成果能够看到,deinit办法有被调用到,也就没有了循环引证。

2.运用闭包的捕获列表,在捕获列表中声明对引证的实例目标为unowned引证。

Swift进阶(五)—— 内存管理

捕获列表

什么是闭包的捕获列表呢?

  • 默许状况下,闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。您能够使⽤捕获列表来显式操控怎样在闭包中捕获值。
  • 在参数列表之前,捕获列表被写为⽤逗号括起来的表达式列表,并⽤⽅括号括起来。假如使⽤捕获列表,则即使省掉参数称号,参数类型和回来类型,也必须使⽤in关键字。

闭包的捕获列表使用如下:

Swift进阶(五)—— 内存管理

创立闭包时,将初始化捕获列表中的条⽬。关于捕获列表中的每个条⽬,将常量初始化为在周围范围内具有相同称号的常量或变量的值。捕获列表的常量有以下几个特点:

  • 捕获列表中的常量是值拷贝,而不是引证
  • 捕获列表中的常量的相当于仿制了变量的值
  • 捕获列表中的常量是只读的,即不行修正

创立闭包时,内部作⽤域中的age会⽤外部作⽤域中的age的值进⾏初始化,但它们的值未以任何特别⽅式衔接。这意味着更改外部作⽤域中的age的值不会影响内部作⽤域中的age的值,也不会更改关闭内部的值,也不会影响关闭外部的值。相⽐之下,只要⼀个名为height的变量,既外部作⽤域中的height,在闭包内部或外部进⾏的更改在两个地⽅均可⻅。