强引用
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
的引⽤计数:
输出的查看t的引⽤计数refCounts
为什么是0x0000000600000002
?
通过源码进行分析,打开
HeapObhect.h
,看到一个宏
HeapObhect.h进入
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS
宏定义,这里看到refCounts
类型是InlineRefCounts
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS进入
InlineRefCounts
定义,它是RefCounts
类型的别名
InlineRefCounts进入
RefCounts
定义,它是一个模板类。后续逻辑取决于模板参数RefCountBits
,也就是上图中传入的InlineRefCountBits
的类型
RefCounts进入
InlineRefCountBits
定义,它是RefCountBitsT
类型的别名
InlineRefCountBits首先确认
RefCountIsInline
是什么,进入RefCountIsInline
定义,本质上是enum
,只有true
和false
。这里传入的RefCountIsInline
就是true
RefCountIsInline再进入到
RefCountBitsT
的定义,里面的成员变量bits
,类型为BitsType
RefCountBitsT
bits
对RefCountBitsInt
的Type
属性取别名,本质上就是uint64_t
类型
RefCountBitsInt明白了
bits
是什么,下面就来分析HeapObject
的初始化方法,重点看第二个参数refCounts
HeapObject初始化方法进入
Initialized
定义,它的本质是一个enum
,找到对应的refCounts
方法,需要分析一下传入的RefCountBits(0, 1)
到底在做什么
Initialized进入
RefCountBits
,还是模板定义,把代码继续往下拉...
RefCountBits在下面找到真正的初始化方法
RefCountBitsT
,传入strongExtraCount
和unownerCount
两个uint32_t
类型参数,将这两个参数根据Offsets
进行位移操作
RefCountBitsT通过源码分析,最终我们得出这样⼀个
结论
:
结论
isImmortal
(0)UnownedRefCount
(1-31):无主引用计数isDeinitingMask
(32):是否进行释放操作StrongExtraRefCount
(33-62):强引用计数UseSlowRC
(63)
对照上述结论,使用二进制查看
refCounts
输出的0x0000000600000002
二进制查看refCounts
1-31
位是UnownedRefCount
无主引用计数33-62
位是StrongExtraRefCount
强引用计数
通过SIL代码,分析
t
的引用计数,当t
赋值给t1
、t2
时,触发了copy_addr
SIL查看SIL文档,
copy_addr
内部又触发了strong_retain
copy_addr回到源码,来到
strong_retain
的定义,它其实就是swift_retain
,其内部是一个宏定义CALL_IMPL
,调用的是_swift_retain_
,然后在_swift_retain_
内部又调用了object->refCounts.increment(1)
strong_retain进入
increment
方法,里面的newbits
是模板函数,其实就是64位
整形。这里我们发现incrementStrongExtraRefCount
方法点不进去,因为编译器不知道RefCountBits
目前是什么类型
increment方法我们需要回到
HeapObject
,从InlineRefCounts
进入,找到incrementStrongExtraRefCount
方法
通过image.pngBitsType
方法将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`的引⽤计数t
赋值给weak
修饰的t1
,查看refCounts
打印出奇怪的地址
通过
LLDB
指令来查看t1
:
使用查看t1weak
修饰的t1
变成了Optional
可选类型,因为当t
被销毁时,t1
会被置为nil
,所以weak
修饰的变量必须为可选类型
通过断点查看汇编代码,发现定义
weak
变量,会调用swift_weakInit
函数
查看汇编代码
通过源码进行分析,找到
swift_weakInit
函数,这个函数由WeakReference
调用,相当于weak
字段在编译器声明过程中自定义了一个WeakReference
对象,目的在于管理弱引用。在swift_weakInit
函数内部调用了ref->nativeInit(value)
, 其中value
就是HeapObject
swift_weakInit进入
nativeInit
方法,判断object
不为空,调用formWeakReference
nativeInit进入
formWeakReference
方法,首先通过allocateSideTable
方法创建SideTable
,如果创建成功,调用incrementWeak
formWeakReference进入
allocateSideTable
方法,先通过refCounts
拿到原有的引用计数,再通过getHeapObject
创建SideTable
,将地址传入InlineRefCountBits
方法
allocateSideTable进入
InlineRefCountBits
方法,将参数SideTable
的地址,直接进行偏移,然后存储到内存中,相当于将SideTable
直接存储到uint64_t的变量中
InlineRefCountBits
之前查看
t
的refCounts
,打印出0xc0000000200d1d6e
这串奇怪的地址,去掉62位
和63位
保留字段,剩余的就是偏移后的HeapObjectSideTableEntry
实例对象的内存地址,即散列表的地址
二进制查看refCounts
回到源码分析,进入
HeapObjectSideTableEntry
定义,里面有object
对象和refCounts
,refCounts
是一个SideTableRefCounts
类型
HeapObjectSideTableEntry进入
SideTableRefCounts
定义,它是RefCounts
类型的别名,和之前分析的InlineRefCountBits
类似,后续逻辑取决于模板参数的传入,这里传入的是SideTableRefCountBits
类型
SideTableRefCounts进入
SideTableRefCountBits
定义,它继承于RefCountBitsT
SideTableRefCountBitsRefCountBitsT
存储的是uint64_t
类型的64位
的信息,用于记录原有引用计数。除此之外SideTableRefCountBits
自身还有一个uint32_t
的weakBits
,用于记录弱引用计数
还原散列表地址,查看弱引用
refCounts
- 将
0xc0000000200d1d6e
地址62位
和63位
的保留字段清零,得到地址0x200D1D6E
- 将
0x200D1D6E
左移3位,还原成HeapObjectSideTableEntry
对象地址0x10068EB70
,也就是散列表地址- 通过
x/8g
读取地址0x10068EB70
查看弱引用refCounts
循环引用
案例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
是常量,并且进行了值拷贝。对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。