swift指针&内存管理-闭包的循环引用
无主引用
和弱引用类似,无主引用不会牢牢保持引用的实例。但是不像弱应用,无主引用假定是永远有值的
当我们去访问一个无主引用的时候,总是假定有值的,所以就可能会发生程序的崩溃
如果两个对象的生命周期并不相关,使用weak
如果非强引用对象 拥有与强引用对象相同或更长的声明周期的话,则应使用 无主引用 unowned (也就是说 两个对象拥有关联 --- unowned)
image.png结果
IFLObj1 deinit
IFLObj2 deinit
obj1 先销毁,obj2后销毁, obj2与obj1有关联,IFLObj2的成员obj1 与 IFLObj1关联在一起,不是可选值
obj1的成员 obj2 是可选值,也就是说 obj1销毁,成员obj2也一定不存在了
因此,IFLObj2的成员 obj1可以用 无主引用 unowned
闭包循环引用
首先我们的闭包一般默认会捕获我们的外部变量
var mVar = 10
let closure1 = {
mVar += 1
}
closure1()
print("mVar = \(mVar)")
结果
mVar = 11
从打印结果可以看出来
闭包内部对变量的修改将会改变外部原始变量的值
那同样就会有一个问题,如果我们在class内部定义一个闭包,当前闭包访问属性的过程中,就会对我们当前的实例对象进行捕获
class IFLObj1 {
var a: Int = 21
var b: String = "joewong"
var mClosure: (() -> ())?
deinit {
print("IFLObj1 deinit")
}
}
func testClosure() {
let mObj1 = IFLObj1()
mObj1.mClosure = {
mObj1.a += 2
}
}
testClosure()
IFLObj1 deinit 并未执行
image.png控制台查看 mObj1 引用计数
2 = (1 << 33 == 2)
mObj1 的强引用计数 为 1
注意:lldb调试的时候,不能使用 po mObj1,
因为那会增加 mObj1的引用计数,对分析造成干扰,而应该采用 api Unmanaged.passUnretained, passUnretained意思就是不对 mObj1造成引用计数+1
我们对比下不给 IFLObj1 成员 mClosure 赋值的情况
image.png没有对 IFLObj1 成员 mClosure 赋值的情况下
强引用计数为0, 无主引用为1
通过对比,闭包的初始化 就会对 所在类 实例对象进行捕获,而并不需要等到闭包执行时,才捕获
如果用po 直接查看的话,会看到 闭包初始化之后,引用计数为2,未初始化闭包前,引用计数为1
这样就可以解释,testClosure函数作用域内,引用计数为2,作用域结束之后,引用计数 - 1, 变为1,并未变成0,所以无法执行 deinit
而能这样去理解吗???
从逻辑上就可以推翻这种假设了,
那如果 直接po mObj1, po多次,就会增加多次引用,作用域结束的时候,引用计数-1,并没有减到0,deinit并没有执行,假设就是错的,不能这样简单取巧的方式去理解
为了更严谨,我们就需要知道 deinit 是如何被调用执行
分析deinit调用时机
我们先通过汇编查看以下 testClosure 作用域结束前的 汇编流程,然后再找线索切入源码查看
image.png image.pngswift_bridgeObjectRelease 是 OC 与 swift之间的转换部分,我们现在的代码是swift,忽略掉这几个影响,直接跳转到 swift_release
进入swift_release 指令
image.pngimage.png
这个时候指令跳转进入到 swift_release
image.png image.png关键线索出现, 可是试图通过这种方式去源码搜索关键字,分析源码逻辑,纯粹从逻辑理性角度去分析,结果就是 nothiing,什么也得不出来,除了得到一些自己想当然的垃圾逻辑,基本上都是错的,有主观臆想在里边
这个时候,需要借助于符号断点 + 推断流程 + 部分源码,当然了,要摒弃掉po mObj1 带来的引用计数的影响
单步符号调试+引用计数监测
为了更方便查看引用计数的变化,testClosure 作用域里,我们追加一个 mObj2的引用
deinit 敲上断点
image.png image.png下载这两个符号 重新调试 , testClosure 作用域结束前打开 下载的两个符号
image.pngtestClosure作用域内,
mObj1 无主引用计数为1
强引用计数 为 1, [ 1 << 33, 在高32位显示为2]
image.pngarrayDestroy, 与目前我们关注的对象不符,忽略
image.png此时,引用计数没有变化,因为还为执行 回收
image.pngimage.png
引用计数没有变化
image.png引用计数发生变化, 32位显示为1,为标识位,标识当前正在进行deinit
deinit 执行
再看下 swift_deallocObjectImpl 源码
image.png image.png至于 deinit 基于什么样的源码逻辑 调用执行,暂时可以放弃这个念头,太繁琐,没有直给的逻辑,但是从前面的调试流程,可以知道
deallocClassInstance 引用计数清零,deinit标识位设置为1, 然后调用deinit
回到之前的闭包循环引用问题
通过符号进入 swift_deallocObjectImpl
image.png引用计数依然为2
image.png此时 引用计数 显示的是 0
image.png但是 deinit 标识位 并没有设置为1, deinit未执行
分析下来,所以关键是这个 第32位标识位 ,为1,才会调用deinit去执行
在以上 闭包初始化前提下的分析过程中,swift_deallocObject 并未执行,反而执行的是swift_slowDealloc
源码中有这样的逻辑
image.png image.pngdeinit 标识位 不为1,就没有机会执行 deinit
闭包捕获列表
默认情况下,闭包表达式从其周围的范围捕获常量和变量,并强引用这些值,我们可以使用捕获列表来显式控制如何在闭包中捕获值
在参数列表之前,捕获列表被写为用逗号括起来的表达式列表,并用方括号括起来。如果使用捕获列表,则即使省略参数名称,参数类型和返回类型,也必须使用关键字in
var a1 = 0
var h1 = 13.1
let closure1 = { [a1] in
print("closure1, a1 = \(a1), h1 = \(h1)")
}
a1 = 10
h1 = 18.9
closure1()
结果
closure1, a1 = 0, h1 = 18.9
闭包在初始化时,就直接对 捕获列表中的参数进行了初始化,而并不是在闭包执行时才初始化参数列表
也就是说,closure1 在初始化时, 捕获列表中的 参数 a1就已经完成了初始化,这里的逻辑是
[let a1 = 0], 是个常量, 即使closure1执行时, 这个参数也不会再变了
而 非捕获列表中的变量,比如 h1的捕获 则发生在 closure1执行时,这时候就是实际h1的值了
如果改变一下
var a1 = 0
var h1 = 13.1
let closure1 = { [a1] in
print("closure1, a1 = \(a1), h1 = \(h1)")
}
a1 = 10
h1 = 18.9
closure1()
结果
closure1, a1 = 10, h1 = 18.9
闭包-延长生命周期
class IFLObj1 {
var a: Int = 21
var b: String = "joewong"
var mClosure: (() -> ())?
deinit {
print("IFLObj1 deinit")
}
}
func testClosure() {
let mObj1 = IFLObj1()
mObj1.mClosure = { [weak mObj1] in
mObj1!.a += 2
}
mObj1.mClosure!()
print("------mObj1.a = \(mObj1.a)")
}
testClosure()
结果
------mObj1.a = 23
IFLObj1 deinit
类似于OC 的方式,在block 内部 声明强引用,延长生命周期
mObj1.mClosure = { [weak mObj1] in
if let mObj1 = mObj1 {
mObj1.a += 2
}
}
api - withExtendedLifetime 延长生命周期
withExtendedLifetime(mObj1) {
if let mObj1 = mObj1 {
mObj1.a += 2
}
}