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


    swift指针&内存管理-引用

    无主引用

    和弱引用类似,无主引用不会牢牢保持引用的实例。但是不像弱应用,无主引用假定是永远有值的

    当我们去访问一个无主引用的时候,总是假定有值的,所以就可能会发生程序的崩溃

    如果两个对象的生命周期并不相关,使用weak

    如果非强引用对象 拥有与强引用对象相同或更长的声明周期的话,则应使用 无主引用 unowned (也就是说 两个对象拥有关联 — unowned)

    在这里插入图片描述

    结果

    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)")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    IFLObj1 deinit 并未执行

    在这里插入图片描述

    控制台查看 mObj1 引用计数

    2 = (1 << 33 == 2)

    mObj1 的强引用计数 为 1

    注意:lldb调试的时候,不能使用 po mObj1,

    因为那会增加 mObj1的引用计数,对分析造成干扰,而应该采用 api Unmanaged.passUnretained, passUnretained意思就是不对 mObj1造成引用计数+1

    我们对比下不给 IFLObj1 成员 mClosure 赋值的情况

    在这里插入图片描述

    没有对 IFLObj1 成员 mClosure 赋值的情况下

    强引用计数为0, 无主引用为1

    通过对比,闭包的初始化 就会对 所在类 实例对象进行捕获,而并不需要等到闭包执行时,才捕获

    如果用po 直接查看的话,会看到 闭包初始化之后,引用计数为2,未初始化闭包前,引用计数为1

    这样就可以解释,testClosure函数作用域内,引用计数为2,作用域结束之后,引用计数 - 1, 变为1,并未变成0,所以无法执行 deinit

    而能这样去理解吗???

    从逻辑上就可以推翻这种假设了,

    那如果 直接po mObj1, po多次,就会增加多次引用,作用域结束的时候,引用计数-1,并没有减到0,deinit并没有执行,假设就是错的,不能这样简单取巧的方式去理解

    为了更严谨,我们就需要知道 deinit 是如何被调用执行

    分析deinit调用时机

    我们先通过汇编查看以下 testClosure 作用域结束前的 汇编流程,然后再找线索切入源码查看

    在这里插入图片描述

    在这里插入图片描述

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

    进入swift_release 指令

    在这里插入图片描述
    在这里插入图片描述
    这个时候指令跳转进入到 swift_release

    在这里插入图片描述
    在这里插入图片描述

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

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

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

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

    deinit 敲上断点

    在这里插入图片描述

    在这里插入图片描述
    下载这两个符号 重新调试 , testClosure 作用域结束前打开 下载的两个符号

    在这里插入图片描述

    testClosure作用域内,

    mObj1 无主引用计数为1

    强引用计数 为 1, [ 1 << 33, 在高32位显示为2]
    在这里插入图片描述

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

    在这里插入图片描述

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

    在这里插入图片描述
    在这里插入图片描述

    引用计数没有变化

    在这里插入图片描述

    引用计数发生变化, 32位显示为1,为标识位,标识当前正在进行deinit

    deinit 执行

    再看下 swift_deallocObjectImpl 源码

    在这里插入图片描述
    在这里插入图片描述

    至于 deinit 基于什么样的源码逻辑 调用执行,暂时可以放弃这个念头,太繁琐,没有直给的逻辑,但是从前面的调试流程,可以知道

    deallocClassInstance 引用计数清零,deinit标识位设置为1, 然后调用deinit

    回到之前的闭包循环引用问题

    通过符号进入 swift_deallocObjectImpl

    在这里插入图片描述

    引用计数依然为2

    在这里插入图片描述

    此时 引用计数 显示的是 0

    在这里插入图片描述

    但是 deinit 标识位 并没有设置为1, deinit未执行

    分析下来,所以关键是这个 第32位标识位 ,为1,才会调用deinit去执行

    在以上 闭包初始化前提下的分析过程中,swift_deallocObject 并未执行,反而执行的是swift_slowDealloc

    源码中有这样的逻辑

    在这里插入图片描述

    在这里插入图片描述

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果

    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    结果

    ------mObj1.a = 23

    IFLObj1 deinit

    类似于OC 的方式,在block 内部 声明强引用,延长生命周期

    mObj1.mClosure = { [weak mObj1] in
        if let mObj1 = mObj1 {
            mObj1.a += 2
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    api - withExtendedLifetime 延长生命周期

    withExtendedLifetime(mObj1) {
        if let mObj1 = mObj1 {
            mObj1.a += 2
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    MATLAB程序设计之循环结构入门详解
    Java开发的模板引擎--freemarker
    Django基础三之路由、视图、模板
    时空智友企业信息管理系统任意文件读取漏洞复现
    关于如何在vue中使用typescript(第一天)
    【RabbitMQ】WorkQueue
    springboot接收前端传参的几种方式
    极简人体感应芯片-DLT8P68SA-杰力科创
    2022千元无线蓝牙耳机,音质超高的千元蓝牙耳机品牌
    QPlainTextEdit等 自制 文本或者代码 编辑器控件 是如何 自定义 实现的?
  • 原文地址:https://blog.csdn.net/qq_42431419/article/details/128020823