美文网首页
Swift内存管理

Swift内存管理

作者: YY323 | 来源:发表于2021-01-05 18:59 被阅读0次

    Swift 中使用自动引用计数(ARC)机制来追踪和管理内存。

    • 强引用

    class YYTeacher {
        var age : Int = 18
        var name : String = "YY"
    }
    
    var t = YYTeacher()
    var t1 = t
    var t2 = t
    

    通过 lldb 端可知上述代码执行完成后, t 的内存情况如下:

    那么为什么其 refCounts0x0000000600000003 呢?

    在前面分析swift的类结构时,通过SIL查看源码:

    refCountsInlineRefCounts 类型的。

    InlineRefCounts 又是 RefCounts 这个模板类的别名,这个模板类又取决于其传入的参数类型 InlineRefCountBits

    InlineRefCountBits 又是 RefCountBitsT 这个模板类的别名。

    RefCountBitsT 这个类中有一个属性 BitsType 类型的 bits ,通过查看定义可知:

    BitsType 其实是 RefCountBitsInt 这个结构体中 Type 属性的别名,所以 bits 其实就是 uint64_t 类型。

    分析了 RefCountBitsT 这个类中的属性bits,再来分析一下 swift 中的创建对象的底层方法 _swift_allocObject_

    查看 Initialized 的定义可知是一个枚举

    refCounts 对应的构造函数:

    可知在这里真正做事的是 RefCountBits

    点进去可知 RefCountBits 也是一个模板定义,所以真正的初始化操作应该是在 RefCountBitsT 这个类中的构造方法:

    根据 offset 进行的一个位移 操作。
    分析 RefCountBitsT 的结构,如下图:

    • isImmortal(0)
    • UnownedRefCount(1-31):无主引用计数
    • isDeinitingMask(32):是否释放的标记
    • StrongExtraRefCount(33-62):强引用计数
    • UseSlowRC(63)

    这里重点关注 UnownedRefCountStrongExtraRefCount ,将刚刚例子中的 t 的引用计数0x0000000600000003 对比:

    可知例子中代码执行完后,t强引用计数变成了3.

    通过分析例子中的SIL文件:

    通过查看SIL文档

    可知 copy_addr 内部又调用了 一次strong_retain,而 strong_retain 其实就是 swift_retain

    从上图可知最终调用的就是 __swift_retain_ 这个方法,再往下走:


    综合上述可知:__swift_retain_ 其实就是强引用计数增加了1

    注意:如果使用 CFGetRetainCount(t) 来获取 t 的强引用计数, t 的强引用计数会在原来的基础上 +1

    • 弱引用

    使用关键字weak声明弱引用的变量,

    weak var t = YYTeacher()
    

    从上图可知弱引用声明的变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为 nil的。如下面的例子:

    class YYTeacher {
        var age : Int = 18
        var name : String = "YY"
        
        deinit {
            print("YYTeacher deinit")
        }
    }
    
    var t = YYTeacher()
    t = nil
    

    上面的代码在t = nil处会报错,可是如果将变量 t 声明成可选类型,再将 t = nil 则不会报错,所以意味着 weak声明的变量必须是一个可选类型,才能被允许被设置为 nil

    通过源码分析 weak 关键字在底层到底做了什么?

    class YYTeacher {
        var age : Int = 18
        var name : String = "YY"
    }
    
    var t = YYTeacher()
    weak var t1 = t
    

    汇编模式下可知调用了 swift_weakInit 这个方法

    通过 SIL 中查看源码看一下 swift_weakInit() 这个方法究竟做了什么?

    到这里,就引出了 HeapObjectSideTableEntry 这个类

    SideTableRefCountBits决定了SideTableRefCounts的真实类型,继承自 RefCountBitsT ,多了一个 uint32_t 的属性 weakBits

    class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
    {
      uint32_t weakBits;
    // ...
    }
    

    综上可知:HeapObjectSideTableEntry 其实就是原来的 uint64_t 再加上一个存储弱引用计数的 uint32_t

    了解 HeapObjectSideTableEntry 后,接着看 allocateSideTable() 方法中的实现,在新建了一个 SideTable对象后,将其作为参数传入调用了 InlineRefCountBits() 这个构造方法:

    HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
    {
      auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
      // ...
      // FIXME: custom side table allocator
      HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
      
      auto newbits = InlineRefCountBits(side);
    // ...
    

    前面学习了 HeapObject 里的 RefCounts 实际上是InlineRefCountBits 的一个模板参数,在这里构造完 Side Table 后,对象中 InlineRefCountBits 就不再是原来的引用计数了,而是一个指向 Side Table 的指针,因为它们都是 uint64_t,所以需要不同的构造函数来区分,这里 InlineRefCountBits 的构造函数如下:

    RefCountBitsT(HeapObjectSideTableEntry* side)
        : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
               | (BitsType(1) << Offsets::UseSlowRCShift)
               | (BitsType(1) << Offsets::SideTableMarkShift))
    {
       assert(refcountIsInline);
    }
    

    其实就是把 Side Table 的地址做了一个偏移 存放到内存中(即把创建的 side地址存放到 uint64_t 这个变量中),将指针地址没用的位置替换成标识位

    扩展:如果此时再增加引用计数会怎样呢?

    通过前面可知:这里在给 t2 赋值的时候,内部会调用
    strong_retain --> swift_retain --> __swift_retain

    void incrementStrong(uint32_t inc) {
      refCounts.increment(inc);
    }
    

    通过查看源码可知此时的 refCounts 其实就是 SideTableRefCounts ,所以这个时候其实操作的就是SideTable

    总结:上面讲了两种 RefCounts

    • InlineRefCounts:用在 HeapObjet 中,其实就是一个uint64_t,没有弱引用时存的是引用计数(strongRefCounts + unownedRefCounts),有弱引用时存的是Side Table 的指针地址

    • HeapObjectSideTableEntry:内部有一个实质为 SideTableRefCountBits类型的属性 refCounts ,该类型中多了一个属性 uint32_t weakBits,继承自 RefCountBitsT ,即原来的 uint64_t 加上一个存储弱引用计数的 uint32_t
      Side Table 的指针地址中存储的是:object + ? + 引用计数(strongRefCounts + unownedRefCounts) + 弱引用计数(weakRefCounts)

    • 循环引用

    var age = 10
    let closure = {
        age += 1
    }
    closure()
    

    上面例子中,执行完闭包后,age的值为11,说明闭包内部对变量的修改将会改变外部原始变量的值,原因是闭包能默认捕获外部变量,和OC中的Block是一样的。

    接下来通过deinit来观察实例对象是否将被释放:
    deinit:在swift中叫做反初始化器,实例变量将要被回收时调用deinit,观察实例对象是否被销毁。

    class YYTeacher {
        var age = 12
        
        deinit {
            print("YYTeacher deinit")
        }
    }
    
    func test() {
        let t = YYTeacher()
        let closure = {
            t.age += 10
        }
    
        closure()
        print(t.age)
    }
    test()
    

    在上面例子中,当执行完函数test后,实例对象t调用了deinit即将被销毁,说明闭包内部调用外部变量会对其做强引用操作。

    那么怎样才会造成循环引用呢?

    class YYTeacher {
        var age = 12
        var completionBack : (()->())?
        
        deinit {
            print("YYTeacher deinit")
        }
    }
    
    func test() {
        let t = YYTeacher()
        t.completionBack = {
            t.age += 10
        }
        
        t.completionBack!()
    }
    
    test()
    

    执行完上面的代码会发现,并没有调用deinit,说明这里有循环引用,导致实例对象能被销毁
    Swift中,通过弱引用(weak)无主引用(unowned)来解决循环引用。
    weak可选类型,实例的生命周期内可为nil,实例销毁后实例对象被置为nil;
    unowned非可选类型,使用前提要确保实例对象初始化后永能为nil,实例销毁后仍存储着实例对象的内存地址,若再访问则会造成野指针错误。

    这里解决循环引用:

    t.completionBack = {[unowned t] in
            t.age += 10
    }
    // 或者
    t.completionBack = {[weak t] in
            t?.age += 10
    }
    
    /** 这里weak t是可选类型,可能为nil,OC中是允许给nil对象发送消息的,而在Swift中是不允许给nil对象发送消息的,所以要加上?,如果为nil则不会执行后面的.age+=1*/
    

    上面的语法在Swift也叫做捕获列表,定义在参数列表之前,用[ ]括起来,若有多个,中间用,连接,即使省略参数名称、参数类型和返回类型也必须加上关键字in

    var i = 0
    var arrClosure : [()->()] = []
    
    for _ in 1...3 {
        arrClosure.append {//[i] in
            print(i)
        }
        i += 1
    }
    
    arrClosure[0]() //3
    arrClosure[1]() //3
    arrClosure[2]() //3
    

    上面例子中,三个闭包捕获的都是最后一次i的值,如果想要打印出来的值为1、2、3,三个闭包捕获的就应该是每次i的值的副本(copy),即捕获列表,闭包表达改为:

    arrClosure.append {[i] in
       print(i)
    }
    

    总结:捕获列表中捕获的是变量的一个副本本地表格;对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。

    相关文章

      网友评论

          本文标题:Swift内存管理

          本文链接:https://www.haomeiwen.com/subject/tzswoktx.html