ios底层原理

swift指针&内存管理-闭包的循环引用

2022-11-23  本文已影响0人  erlich

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.png

swift_bridgeObjectRelease 是 OC 与 swift之间的转换部分,我们现在的代码是swift,忽略掉这几个影响,直接跳转到 swift_release

进入swift_release 指令

image.png
image.png

这个时候指令跳转进入到 swift_release

image.png image.png

关键线索出现, 可是试图通过这种方式去源码搜索关键字,分析源码逻辑,纯粹从逻辑理性角度去分析,结果就是 nothiing,什么也得不出来,除了得到一些自己想当然的垃圾逻辑,基本上都是错的,有主观臆想在里边

这个时候,需要借助于符号断点 + 推断流程 + 部分源码,当然了,要摒弃掉po mObj1 带来的引用计数的影响

单步符号调试+引用计数监测

为了更方便查看引用计数的变化,testClosure 作用域里,我们追加一个 mObj2的引用

deinit 敲上断点

image.png image.png

下载这两个符号 重新调试 , testClosure 作用域结束前打开 下载的两个符号

image.png

testClosure作用域内,

mObj1 无主引用计数为1

强引用计数 为 1, [ 1 << 33, 在高32位显示为2]

image.png

arrayDestroy, 与目前我们关注的对象不符,忽略

image.png

此时,引用计数没有变化,因为还为执行 回收

image.png
image.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.png

deinit 标识位 不为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
    }
}
上一篇下一篇

猜你喜欢

热点阅读