「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
-
本文主要介绍swift中引用计数的变化,来进行内存管理。
-
swift中使用
自动引用计数(ARC)机制来追踪管理内存。 我们初始化一个Person,之后赋值给t,引用计数+1,之后再赋值给t1,引用计数+1,最后再赋值给t2,引用计数+1.我们打印指针指向的内存分布。
我们知道swift的实例对象本质是HeapObject结构体,是由metadata和refcounts组成。上面我画圈的就是它的引用计数的变化。
1. 引用计数的分析
- 源码分析
查看
HeapObject中refcounts是宏定义的InlineRefCounts类型
查看InlineRefCounts的定义
查看RefCounts是一个模版类,接受的参数是RefCountBits
其中BitsType bits这个成员变量,后面的操作都是关于这个变量进行操作
- 初始化的引用计数
我们上面打印的时候赋值给t的时候是0x3,初始化的时候引用计数是多少
-
初始化
-
引用计数
Initialized_t默认初始化的引用计数为1
传入的对象为RefCountBits,实际上进行位域运算,根据定义的offset
如图,已知外部调用 RefCountBitsT 初始化方法,strongExtraCount 传 0,unownedCount 传 1。那么 Offsets::StrongExtraRefCountShift = 33,Offsets::PureSwiftDeallocShift = 0,Offsets::UnownedRefCountShift = 1,
- 其中
RefCountBits的分布如下
所以我们最终计算的结果是
0 << 33 | 1 << 0 | 1 << 1;
0 | 1 | 2 = 3;
2. 强引用
我们在给初始化的对象进行强引用会使引用计数+1,文章开始的时候截图所示。那么怎么变化的呢?
我们编译成sil文件
开始p定义为全局地址,之后初始化成功后store到p,之后赋值是copy_addr操作。
SIL官方文档中关于copy_addr的解释相当于strong_retain
- 其中的
strong_retain对应的就是swift_retain,其内部是一个宏定义,内部是_swift_retain_,其实现是对object的引用计数作+1操作
- increment
从
InlineRefCounts进入,其中是c++中的模板定义,是为了更好的抽象,在其中查找bits(即decrementStrongExtraRefCount方法)
例如以
p的refCounts为例(其中33-62位是strongCount,每次增加强引用计数增加都是在33-62位上增加的,固定的增量为1左移33位,即0x200000000)
- 只有
p时的refCounts是0x0000000200000003 p + p1时的refCounts是0x0000000400000003=0x0000000200000003+0x200000000p + p1 + p2时的refCounts是0x0000000600000003=0x0000000400000003+0x200000000- 针对上面的例子,可以通过
CFGetRetainCOunt获取引用计数,发现依次是 2、3、4,默认多了一个1
当我们使用
p打印的时候也会调用retain,导致引用计数+1
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这个关键词表示这是一个弱引用。
这里注意的是在不在闭包里的话,声明的变量是
全局变量,无法释放的。
由于弱引用不会保持对实例的引用,所以说实例被释放了弱引用仍然引用这个实例对象也是有可能的,ARC会在被引用的实例释放的时候好自动设置弱引用对象为nil,weak是可选的optional,向空对象调用不会报错。
可以检查弱引用的值是否存在,可以像其他可选值一样,永远不会遇到野指针
3.2 弱引用探究
我们在弱引用处断点
查看swift_weakInit在源码的定义返回的是WeakReference的弱引用对象
查看nativeInit主要是获取sideTable之后进行存储。
formWeakReference创建sideTable,成功后插入弱引用对象。
allocateSideTable
- 将创建的sideTable地址给
InlineRefCountBits,并查看其初始化方法
根据sideTable地址 放到64位位域中,作了偏移操作并存储到内存,相当于将sideTable直接存储到了64位的变量中
2个标识位一个UseSlowRCShift:62,一个SideTableMarkShift:63
RefCountBitsT本质一个64位指针,把side存储到64位位域中,并做了一些标记。
-
查看
HeapObjectSideTableEntry存放的是原有的HeapObejct对象和引用计数 -
SideTableRefCountBits存放的是弱引用计数
上面根据得到的散列表会左移3位
SideTableUnusedLowBits宏定义
因此我们打印我们之前的对象地址位0x00000001005053e0打印它的内存分布,在计算器上显示refcounts的地址
还原散列表的话,高位进行抹0
之后右移3位进行还原。
验证打印的结果,和我们数据类型相同。
3.3 小结
可以看到官方关于引用计数的描述分为2种情况
它们的引用计数:
- 无弱引用:
strong RC + unowned RC - 有弱引用:
strong RC + unowned RC + weak RC
4. 无主引用
无主引用和弱引用类似,都是不持有引用对象的,但是无主引用必须保证有值,否则会造成野指针崩溃
根据苹果的官方文档说明:当2个对象的生命周期不一致的时候必须使用weak进行修饰打破循环,但是如果他们的生命周期一致的话可以使用unowned
老师和课程是相互持有的,老师不在课堂课程就没法上了。有在上课的话一定老师,因此他们的生命周期是一致的,可以使用unowned进行修饰。
5. 闭包循环引用
我们在oc中知道block使用不当会造成循环引用问题,那么swift的闭包是否会有同样问题
先看下这个闭包
这里闭包捕获了外部的变量,打印的结果也是修改后的结果。
- 换成class对象的话
捕获了对象,并修改了对象的age的值但是没有执行
deInit,此时t是全局变量没有释放.
5.1 闭包循环引用的解决
此时clourse没有和我们实例对象相互引用所以可以释放,当相互引用时
此时没有打印
dealloc,说明没有释放,闭包强持有了对象。
引用计数发生了变化,强持有了。怎么解决呢
使用weak
也可以使用unowned
5.2 闭包捕获列表
-
[weak t] / [unowned t]在swift中被称为捕获列表捕获列表: -
默认情况下,闭包表达式从其
周围的范围捕获常量和变量,并强引⽤这些值。您可以使⽤捕获列表来显式控制如何在闭包中捕获值。 -
在参数列表之前,捕获列表被写为⽤逗号括起来的
表达式列表,并⽤⽅括号括起来。如果使⽤捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使⽤in关键字。
我们可以发现闭包中打打印的age变量时白色的,相当于我们捕获的age 进行了值拷贝,而height是外部变量相当于指针拷贝,所以我们外部修改也会造成打印的改变。
6.总结
-
swift中对象
创建的时候引用计数就会+1,无弱引用的时候我们是对heapObject中的refcounts进行操作,它是一个64位指针信息,包含了强引用计数和无主引用计数和一些flags。 -
弱引用是对
sideTable进行操作,第一次的时候会创建这个弱引用表,把它放到我们refcounts指针的位置,并对它进行偏移操作。是由实例对象Object+ 强引用+无主引用+弱引用计数组成。 -
对于swift中的
循环引用可以使用weak或者unowned进行修饰打破循环,同一生命周期可以使用unowned,否则使用weak,防止野指针。 -
swift中闭包的
捕获列表是值拷贝