Swift和OC一样,也是采用了依据引证计数的ARC内存管理方案,在OC中ARC引证主要有强引证
和弱引证
,Swift的ARC引证除了强引证
和弱引证
外,又添加了一个无主引证
。
在第一篇文章Swift进阶(一) —— 类与结构体中,咱们知道Swift实质是一个HeapObject
的结构体,而在HeapObject
结构体中有两个成员变量:metadata
和refCounts
。其间metadata
指向元数据目标,存储Swift数据结构类、结构体的基本信息、特点和办法列表等。而这个refCounts
特点则是和ARC的引证计数相关。
refCounts的实质
咱们先从源码上面来查找refCounts详细做了什么。
首要咱们先从HeapObject.h
去找refCounts
特点
咱们能够看到refCounts
的类型是InlineRefCounts
,在RefCount.h
文件中,咱们找到了InlineRefCounts
的界说。
从界说中,咱们能够知道InlineRefCounts
是一个模板类,是一个接收了InlineRefCountBits
类型参数的RefCounts
类。接着咱们去检查RefCounts
类的数据结构
从RefCounts
类中的API咱们能够看到,在这个类里边,都是在操作RefCountsBits
这个传进来的泛型参数,咱们能够知道RefCounts
类实质上是对当时引证计数的一个包装。因而,引证计数的类型取决于传进来的这个参数类型,也便是上面的InlineRefCountBits
类。
经过查找InlineRefCountBits
的界说,咱们知道,这又是一个模板函数。咱们先看一下RefCountIsInline
这个参数。
能够看到这个参数是枚举
类型,要么是true
,要么是false
。接下来咱们检查RefCountBitsT
这个类。
在RefCountBitsT
类中,只要bits
这个成员变量,这个bits
是RefCountBitsInt
类型。因而咱们能够确定引证计数类型应该是RefCountBitsInt
类型,咱们去看一下RefCountBitsInt
类。
能够看到,Type
的类型是一个uint64_t
的位域信息,在这个uint64_t
的位域信息中存储着运行生命周期的相关引证计数。
到了这儿,咱们仍然不知道是怎样设置的,咱们先来看⼀下,当咱们创立⼀个实例目标的时候,当时的引⽤计数是多少?
咱们先来检查HeapObject
类的初始化办法
在HeapObject
类的初始化办法中,咱们看到了一个Initialized
,这是对refCounts
的初始化赋值。咱们查找一下这个Initialized
,发现它是Initialized_t
枚举类型。
查找Initialized_t
,咱们找到了refCounts
的初始化办法
经过上面的注释能够看出来,这儿是给一个新创立的目标的引证计数置为1。传入的参数为RefCountBits(0, 1)
,这儿的RefCountBits
便是咱们上面说的RefCountBitsT
类。咱们去检查RefCountBitsT
类的初始化办法,代码如下:
从上面的初始化办法咱们能够知道,strongExtraCount
是0, unownedCount
是1,初始化时对这两个数进行了偏移,接下来咱们来查找StrongExtraRefCountShift
、
PureSwiftDeallocBitCount
、PureSwiftDeallocShift
这三个偏移数字是多少?
经过全局查找StrongExtraRefCountShift
,咱们找到了这三个常量的界说:
其间PureSwiftDeallocShift
的值是0,PureSwiftDeallocBitCount
的值是1。而StrongExtraRefCountShift
则是经过shiftAfterField
办法核算出来。咱们查找一下shiftAfterField
办法的界说
依据上面的办法界说,咱们能够求出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
终究咱们经过代码验证一下。 经过lldb指令打印地址,咱们能够看到: 打印出来的成果和咱们料想的一样。
强引证
在Swift中,默许状况下,对一个实例目标的引证是强引证。当咱们创立一个实例目标后,它的refcount
特点会是0x0000000000000003
,当咱们对这个实例目标进行强引证后,refcount
特点会发生什么改变呢?
代码如下:
然后分别在t1、t2、print(“end”)处分别设置断点,然后运用lldb指令x/8g
检查t的内存结构,每次过一个断点打印内存结构一次。打印成果如下:
咱们能够看到,当对实例变量t
进行强引证的过程中,t
的refcount
特点从0x0000000000000003
-> 0x0000000200000003
->0x0000000400000003
。经过核算器对这几个地址进行解析,检查它们的改变。
经过核算器咱们能够看到,当引证次数添加时,refcount
特点在高位进行位移操作。
咱们下面经过源码去剖析一下。首要经过汇编代码来检查一下强引证操作怎样完成:
从汇编代码中能够看到,当对一个实例变量强引证时,调用了一个swift_retain
办法。接下来咱们经过源码检查swift_retain
怎样完成。
经过检查swift_retain
函数完成,咱们能够看到,refCounts.increment(1)
,咱们继续检查increment
函数的完成。
在这儿边咱们看到了incrementStrongExtraRefCount
这个办法,从字面意思就能够看出,这个办法是用来添加强引证计数的。咱们深入到这个办法里边,检查怎样添加强引证计数。
咱们能够看到,强引证计数的添加是先把要添加的引证计数参数往左移动33位,然后在和本来的引证计数相加,也便是说每做一次强引证,refcount
特点就会加上0x200000000
。
上面这张图是refcount
特点64位域信息下面的存储方式,咱们能够看到在高位33-62位用于存储强引证计数。而在第32位isDeinitingMask
用来标识这个实例是否能够析构,假如当一个实例不再被强引证,那么它就会被开释掉,咱们来验证一下。代码如下所示:
经过lldb指令打印内存地址,显现如下:
当t = nil
后,refcount
地址显现为0x100000003
,经过核算器进行解析后,咱们发现在32位上标识为1。也便是说这个实例能够被开释掉。
循环引证
咱们在运用OC开发时,经常会遇到运用强引证呈现循环引证导致实例目标无法开释的问题,在Swift中,也会呈现这种状况,比如以下代码所示:
当t = nil
时,不会触发deinit
办法,因为这两个实例目标之间呈现了循环引证。
Swift提供了两种办法⽤来处理你在使⽤类的特点时所遇到的循环强引⽤问题:弱引⽤(weak reference)
和⽆主引⽤(unownedreference)
。
弱引证
弱引⽤不会对其引⽤的实例保持强引⽤,因⽽不会阻⽌ARC
开释被引⽤的实例。这个特性阻⽌了引⽤变为循环强引⽤。声明特点或许变量时,在前⾯加上weak
关键字标明这是⼀个弱引⽤。
因为弱引⽤不会强保持对实例的引⽤,所以说实例被开释了弱引⽤依旧引⽤着这个实例也是有或许的。因而,ARC
会在被引⽤的实例被开释是⾃动地设置弱引⽤为 nil
。因为弱引⽤需要允许它们的值为nil
,它们⼀定得是可选类型。
咱们经过一个事例来看弱引证在内存的存储方式
经过lldb
指令检查存储特点
能够看到,当对一个实例实施弱引证后,refcount
地址发生了很大的改变,那这个改变是怎样来的?
首要咱们先去检查汇编代码:
能够看到,当运用了弱引证后,汇编代码里边调用了swift_weakInit
办法。咱们去源码里边检查这个办法。
这个办法里边调用了nativeInit
办法,咱们再去检查nativeInit
办法
这个代码里边主要是创立了一个side
,然后把这个side
存储起来。所以咱们继续检查formWeakReference
这个办法。
在这个办法里边,经过allocateSideTable
创立一个散列表,然后回来一个HeapObjectSideTableEntry
。
终究咱们检查一下allocateSideTable
办法。
咱们能够看一下这个函数是怎样创立散列表的:
1.首要获取原先的引证计数refcounts
特点。
2.判别refcounts
特点有没有散列表,假如有,则直接回来散列表。
3.假如没有散列表,则重新创立一个散列表,并以此创立一个新的refcounts
特点。
4.对本来的散列表做一些析构处理。
接下来咱们来看一下HeapObjectSideTableEntry
这个类,从源码里边去查找这个类,发现了苹果官方关于引证计数的一些注释:
从注释里边咱们能够知道,当对实例目标强引证的时候,运用了InlineRefCounts
,引证计数核算规则是
strong RC + unowned RC + flags
,而对实例目标就行弱引证后,则变成了HeapObjectSideTableEntry
,引证计数核算规则是strong RC + unowned RC + weak RC + flags
。
咱们来看一下HeapObjectSideTableEntry
类:
这个类里边存储了当时实例目标object
和SideTableRefCounts
类型的refcounts
特点。咱们看一下
SideTableRefCounts
是什么类。
能够看到它也是RefCountBits
的模板类。
看了SideTableRefCounts
的详细完成,咱们能够知道,它是继承了RefCountBitsT
这个类,所以它除了有64位域信息外,还多了一个32位的weakBits
特点,初始化的时候,weakBits
为1,新增一个弱引证,weakBits
加1。
在强引证剖析中,InlineRefCountBits
终究经过RefCountBitsT
这个类来完成,找到和HeapObjectSideTableEntry
相关的初始化办法。
经过检查源码,能够知道UseSlowRCShift
为63,SideTableMarkShift
为62,SideTableUnusedLowBits
值为3。
所以当对一个实例变量进行弱引证后,refCounts
存储方式是这样的:先创立一个散列表,同时把散列表的存储地址右移3位,再把高位63、62位地址置为1,终究把这个地址存储到refCounts
特点中。
接下来,咱们开始对弱引证后的地址进行解析,得到散列表的地址。首要看一下代码
接下来运用lldb指令获取refCounts
的内存地址。
把获取到的内存地址的高63位、62位置为0
再往左移动3位,得到散列表的内存地址。
运用lldb指令解析这个内存地址
所以,当对一个实例目标进行弱引证的时候,实质上是建立了一个散列表。
无主引证
和弱引⽤相似,⽆主引⽤unowned
不会牢牢保持住引⽤的实例。但是不像弱引⽤,总之,⽆主引⽤unowned
假定是永远有值的。
依据苹果的官⽅⽂档的主张。当咱们知道两个目标的⽣命周期并不相关,那么咱们必须使⽤weak
。相反,⾮强引⽤目标拥有和强引⽤目标同样或许更⻓的⽣命周期的话,则应该使⽤unowned
。
假如两个目标的⽣命周期完全和对⽅不要紧(其间⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤weak
假如你的代码能确保:其间⼀个目标毁掉,另⼀个目标也要跟着毁掉,这时候,能够(慎重)⽤unowned
Weak VS unowned
- 假如两个目标的⽣命周期完全和对⽅不要紧(其间⼀⽅什么时候赋值为nil,对对⽅都没影响),请⽤
weak
- 假如你的代码能确保:其间⼀个目标毁掉,另⼀个目标也要跟着毁掉,这时候,能够(慎重)⽤
unowned
闭包的循环引证
在Swift中,创立一个闭包会⼀般默许捕获咱们外部的变量,如下代码所示:
从打印成果能够看出来,闭包内部对变量的修正将会改变外部原始变量的值。这样咱们就会遇到一个问题,假如咱们在class
的内部界说⼀个闭包,当时闭包拜访特点的过程中,就会对咱们当时的实例目标进⾏捕获:
如上图所示,打印的成果没有deinit
办法,也便是说明这个实例目标和闭包形成了循环引证,程序完毕后无法进行开释。
那咱们应该怎样处理循环引证呢?
1.运用闭包的捕获列表,在捕获列表中声明对引证的实例目标为weak
引证。
从打印成果能够看到,deinit
办法有被调用到,也就没有了循环引证。
2.运用闭包的捕获列表,在捕获列表中声明对引证的实例目标为unowned
引证。
捕获列表
什么是闭包的捕获列表呢?
- 默许状况下,闭包表达式从其周围的范围捕获常量和变量,并强引⽤这些值。您能够使⽤捕获列表来显式操控怎样在闭包中捕获值。
- 在参数列表之前,捕获列表被写为⽤逗号括起来的表达式列表,并⽤⽅括号括起来。假如使⽤捕获列表,则即使省掉参数称号,参数类型和回来类型,也必须使⽤in关键字。
闭包的捕获列表使用如下:
创立闭包时,将初始化捕获列表中的条⽬。关于捕获列表中的每个条⽬,将常量初始化为在周围范围内具有相同称号的常量或变量的值。捕获列表的常量有以下几个特点:
- 捕获列表中的常量是值拷贝,而不是引证
- 捕获列表中的常量的相当于仿制了变量的值
- 捕获列表中的常量是只读的,即不行修正
创立闭包时,内部作⽤域中的age
会⽤外部作⽤域中的age
的值进⾏初始化,但它们的值未以任何特别⽅式衔接。这意味着更改外部作⽤域中的age
的值不会影响内部作⽤域中的age
的值,也不会更改关闭内部的值,也不会影响关闭外部的值。相⽐之下,只要⼀个名为height
的变量,既外部作⽤域中的height
,在闭包内部或外部进⾏的更改在两个地⽅均可⻅。