Swift内存管理

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
{
  uint32_t weakBits;
// ...
}

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

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

HeapObjectSideTableEntry* RefCounts::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(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内存管理)