强引用
Swift
使用ARC管理内存
OC
创建实例对象,默认引用计数为0
Swift
创建实例对象,默认引用计数为1
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
}
var t=LGTeacher()
var t1=t
var t2=t
上述代码,通过
LLDB
指令来查看t
的引⽤计数:
输出的refCounts
为什么是0x0000000600000002
?
通过源码进行分析,打开
HeapObhect.h
,看到一个宏
进入
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS
宏定义,这里看到refCounts
类型是InlineRefCounts
进入
InlineRefCounts
定义,它是RefCounts
类型的别名
进入
RefCounts
定义,它是一个模板类。后续逻辑取决于模板参数RefCountBits
,也就是上图中传入的InlineRefCountBits
的类型
进入
InlineRefCountBits
定义,它是RefCountBitsT
类型的别名
首先确认
RefCountIsInline
是什么,进入RefCountIsInline
定义,本质上是enum
,只有true
和false
。这里传入的RefCountIsInline
就是true
再进入到
RefCountBitsT
的定义,里面的成员变量bits
,类型为BitsType
bits
对RefCountBitsInt
的Type
属性取别名,本质上就是uint64_t
类型
明白了
bits
是什么,下面就来分析HeapObject
的初始化方法,重点看第二个参数refCounts
进入
Initialized
定义,它的本质是一个enum
,找到对应的refCounts
方法,需要分析一下传入的RefCountBits(0, 1)
到底在做什么
进入
RefCountBits
,还是模板定义,把代码继续往下拉...
在下面找到真正的初始化方法
RefCountBitsT
,传入strongExtraCount
和unownerCount
两个uint32_t
类型参数,将这两个参数根据Offsets
进行位移操作
通过源码分析,最终我们得出这样⼀个
结论
:
isImmortal
(0)UnownedRefCount
(1-31):无主引用计数isDeinitingMask
(32):是否进行释放操作StrongExtraRefCount
(33-62):强引用计数UseSlowRC
(63)
对照上述结论,使用二进制查看
refCounts
输出的0x0000000600000002
1-31
位是UnownedRefCount
无主引用计数33-62
位是StrongExtraRefCount
强引用计数
通过SIL代码,分析
t
的引用计数,当t
赋值给t1
、t2
时,触发了copy_addr
查看SIL文档,
copy_addr
内部又触发了strong_retain
回到源码,来到
strong_retain
的定义,它其实就是swift_retain
,其内部是一个宏定义CALL_IMPL
,调用的是_swift_retain_
,然后在_swift_retain_
内部又调用了object->refCounts.increment(1)
进入
increment
方法,里面的newbits
是模板函数,其实就是64位
整形。这里我们发现incrementStrongExtraRefCount
方法点不进去,因为编译器不知道RefCountBits
目前是什么类型
我们需要回到
HeapObject
,从InlineRefCounts
进入,找到incrementStrongExtraRefCount
方法
通过BitsType
方法将inc
类型转换为uint64_t
,通过Offsets
偏移StrongExtraRefCountShift
,等同于1<<33
,十进制的1
左移33
位,再转换为十六进制,得到结果0x200000000
。故此上述代码相当于bits += 0x200000000
,左移33
位后,在33-62
位上,强引用计数+1
上述源码分析中,多次看到
C++
的模板定义,其目是为了更好的抽象,实现代码重用机制的一种工具。它可以实现类型参数化,即把类型定义为参数, 从而实现真正的代码可重用性。模版可以分为两类,一个是函数模版,另外一个是类模版。
通过
CFGetRetainCount
查看引用计数
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
}
var t=LGTeacher()
print(CFGetRetainCount(t))
var t1=t
print(CFGetRetainCount(t))
var t2=t
print(CFGetRetainCount(t))
//输出以下内容:
//2
//3
//4
上述代码中,原本
t
的引用计数为3
,使用CFGetRetainCount
方法会导致t
的引用计数+1
弱引用
class LGTeacher{
var age: Int = 18
var name: String = "Zang"
var stu: LGStudent?
}
class LGStudent {
var age = 20
var teacher: LGTeacher?
}
func test(){
var t=LGTeacher()
weak var t1=t
print(CFGetRetainCount(t))
}
test()
//输出以下内容:
//2
上述代码,
t
创建实例对象引用计数默认为1
,使用CFGetRetainCount
查看引用计数+1
,打印结果为2
。显然将t
赋值给使用weak
修饰的t1
,并没有增加t
的强引用计数
通过
LLDB
指令来查看t
的引⽤计数:
将t
赋值给weak
修饰的t1
,查看refCounts
打印出奇怪的地址
通过
LLDB
指令来查看t1
:
使用weak
修饰的t1
变成了Optional
可选类型,因为当t
被销毁时,t1
会被置为nil
,所以weak
修饰的变量必须为可选类型
通过断点查看汇编代码,发现定义
weak
变量,会调用swift_weakInit
函数
通过源码进行分析,找到
swift_weakInit
函数,这个函数由WeakReference
调用,相当于weak
字段在编译器声明过程中自定义了一个WeakReference
对象,目的在于管理弱引用。在swift_weakInit
函数内部调用了ref->nativeInit(value)
, 其中value
就是HeapObject
进入
nativeInit
方法,判断object
不为空,调用formWeakReference
进入
formWeakReference
方法,首先通过allocateSideTable
方法创建SideTable
,如果创建成功,调用incrementWeak
进入
allocateSideTable
方法,先通过refCounts
拿到原有的引用计数,再通过getHeapObject
创建SideTable
,将地址传入InlineRefCountBits
方法
进入
InlineRefCountBits
方法,将参数SideTable
的地址,直接进行偏移,然后存储到内存中,相当于将SideTable
直接存储到uint64_t的变量中
之前查看
t
的refCounts
,打印出0xc0000000200d1d6e
这串奇怪的地址,去掉62位
和63位
保留字段,剩余的就是偏移后的HeapObjectSideTableEntry
实例对象的内存地址,即散列表的地址
回到源码分析,进入
HeapObjectSideTableEntry
定义,里面有object
对象和refCounts
,refCounts
是一个SideTableRefCounts
类型
进入
SideTableRefCounts
定义,它是RefCounts
类型的别名,和之前分析的InlineRefCountBits
类似,后续逻辑取决于模板参数的传入,这里传入的是SideTableRefCountBits
类型
进入
SideTableRefCountBits
定义,它继承于RefCountBitsT
RefCountBitsT
存储的是uint64_t
类型的64位
的信息,用于记录原有引用计数。除此之外SideTableRefCountBits
自身还有一个uint32_t
的weakBits
,用于记录弱引用计数
还原散列表地址,查看弱引用
refCounts
- 将
0xc0000000200d1d6e
地址62位
和63位
的保留字段清零,得到地址0x200D1D6E
- 将
0x200D1D6E
左移3位,还原成HeapObjectSideTableEntry
对象地址0x10068EB70
,也就是散列表地址- 通过
x/8g
读取地址0x10068EB70
循环引用
案例1:
闭包捕获外部变量
var age = 10
let clourse = {
age += 1
}
clourse()
print(age)
//输出以下内容:
//11
从输出结果来看, 闭包内部对变量的修改将会改变外部原始变量的值,因为闭包会捕获外部变量,这个与
OC
中的block
一致
案例2:
deinit
反初始化器
class LGTeacher{
var age = 18
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
}
test()
//输出以下内容:
//LGTeacher deinit
当
test
函数里的局部变量t
被销毁时,会执行反初始化器deinit
方法,这个与OC
中的dealloc
一致
案例3:
闭包修改实例变量的值,闭包能否对
t
造成强引用?
class LGTeacher{
var age = 18
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
let closure = {
t.age += 1
}
closure()
print("age:\(t.age)")
}
test()
//输出以下内容:
//age:19
//LGTeacher deinit
从输出结果来看, 闭包对
t
并没有造成强引用
案例4
将
案例3
进行修改,在LGTeacher
类里定义闭包类型属性completionBlock
,在test
函数内,调用t.completionBlock
闭包,内部修改t.age
属性,这样能否对t
造成强引用?
class LGTeacher{
var age = 18
var completionBlock: (() ->())?
deinit{
print("LGTeacher deinit")
}
}
func test(){
var t = LGTeacher()
t.completionBlock = {
t.age += 1
}
print("age:\(t.age)")
}
test()
//输出以下内容:
//age:18
从输出结果来看,这里产生了循环引用,没有执行
deinit
方法,也没有打印LGTeacher deinit
。因为实例变量t
的释放,需要等待completionBlock
闭包的作用域释放,但闭包又被实例对象强引用,造成循环引用,t
对象无法被释放
案例5
案例4
中循环引用的两种解决方法
1、使用
weak
修饰闭包传入的参数,参数的类型是Optional
可选类型
func test(){
var t = LGTeacher()
t.completionBlock = { [weak t] in
t?.age += 1
}
print("age:\(t.age)")
}
//输出以下内容:
//age:18
//LGTeacher deinit
2、使用
unowned
修饰闭包参数,与weak
的区别在于unowned
不允许被设置为nil,在运行期间假定它是有值的,所以使用unowned
修饰要注意野指针的情况
func test(){
var t = LGTeacher()
t.completionBlock = { [unowned t] in
t.age += 1
}
print("age:\(t.age)")
}
//输出以下内容:
//age:18
//LGTeacher deinit
捕获列表
[unowned t]
、[weak t]
在Swift
中叫做捕获列表
- 捕获列表的定义在参数列表之前
- 书写形式:⽤⽅括号括起来的表达式列表
- 如果使⽤捕获列表,即使省略参数名称、参数类型和返回类型,也必须使⽤
in
关键字[weak t]
就是获取t
的弱引用对象,相当于OC
中的weakself
var age = 0
var height = 0.0
let closure = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
closure()
//输出以下内容:
//0
//1.85
上述代码中,捕获列表的
age
是常量,并且进行了值拷贝。对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。