9. 内存管理&强引用&弱引用&无主引用

377 阅读10分钟

一、内存管理

我们都知道iOS的内存管理是基于引用计数,那么在底层它是如何管理的呢?

1. 案例代码

image-20220109135640763

之前在分析类的初始化流程中,我们知道Swift源码中对象的初始化方法中最后返回了HeapObject对象。这个 HeapObject有两个属性:metadatarefCounts。前面8字节是metadata,后面8字节是refCounts(红框部分),那么refCounts0x0000000000000002是如何来的呢?我们首先分析一下引用计数的存储方式:

2. 引用计数的存储方式

首先我们定位到HeapObject的定义:

image-20220109134506474

这里可以看到HeapObject有两个属性:metadatarefCounts。这个refCountsInlineRefCounts类型。我们进一步查看InlineRefCounts

image-20220109134921989

这里可以看到InlineRefCounts其实是一个模板类RefCounts,它有一个泛型参数InlineRefCountBits,我们先看下RefCounts

image-20220109140435678

这里可以看到RefCounts仅仅是对InlineRefCounts一个包装,而引用计数的具体类型取决于传进来的泛型参数RefCountBits也是前面的InlineRefCountBits。我们继续查看InlineRefCountBits

image-20220109141318200

这里可以看到InlineRefCountBits也是一个模板类RefCountBitsT,它有一个泛型参数RefCountIsInline,这个泛型参数的定义是enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };,也就是说它传入的其实是Bool值。我们接着看RefCountBitsT

image-20220109141917385

这里可以看到RefCountBitsT有一个属性bits,我们查看bits

image-20220109153439888

这里可以看到Type其实是一个64位的位域信息(与OC一样)。由此我们可以得出一个结论,Swift中引用计数管理类是RefCountBitsT,它有一个64位的属性bits用来存储一个对象运行生命周期相关的引用计数。这一点弄清楚之后我们再分析一下案例中refCounts0x0000000000000002的由来:

3. 引用计数的计算方式

首先我们看一下对象的创建:

image-20220109155351920

之前在分析类的初始化流程中我们知道类的创建最终会到_swift_allocObject_中,在这里会调初始化方法new (object) HeapObject(metadata);,继续查看这个初始化方法:

image-20220109155743756

这里可以看到初始化赋值了Initialized,查看这个Initialized

image-20220109160216676

这里可以看到这个Initialized其实是枚举类型Initialized_t,它最终传的是refCounts(RefCountBits(0, 1)),而这个RefCountBits其实就是我们之前讲的RefCountBitsT,查看RefCountBitsT的初始化方法:

image-20220109191316747

这里可以看到初始化方法是对传进的两个参数strongExtraCountunownedCount做位操作。查看源码(RefCount.h)可以得知,StrongExtraRefCountShift为33,UnownedRefCountShift为1。带入计算可以得到2,这就是案例中refCounts0x0000000000000002的原因。

4. 引用计数的内存布局

img

引用计数的位域信息如上图所示,isImmortalUseSlowRC分别占据第0和第63位;无主引用1~31位;强引用33~62位;正在释放在第32位。这里我们验证一下正在释放的对象引用计数:

image-20220109205531934

这里断点调试可以看到当p = nil后,引用计数是0x0000000100000002,复制到计算器中查看:

image-20220109205744859

这里可以看到第32位是1表示正在销毁。

二、强引用(strong)

Swift中对象在赋值时默认就是强引用。

1. 案例代码

这里我们添加调试代码:

image-20220110155826995

2. 底层分析

断点进汇编:

image-20220110155923371

这里可以看到,强引用会调swift_retain,我们再进源码看一下:

image-20220110160109427

最后调到源码的_swift_retain_,这里调了refCounts.increment(1),我们接着看increment方法:

image-20220110160441056

increment可以看到调了incrementStrongExtraRefCount,我们再看incrementStrongExtraRefCount

image-20220110161717332

这里可以看到强引用在做位运算,原引用计数加1后左移33位。

三、弱引用(weak)

弱引用不会对其引用的实例保持强引用,因而不会阻止ARC 释放被引用的实例,这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 关键字表明这是一个弱引用。由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC 会在被引用的实例被释放是自动设置弱引用为 nil 。由于弱引用需要允许它们的值为 nil , 它们一定得是可选类型。

1. 案例代码

这里我们先添加案例代码:

image-20220110163525434

这里可以看出弱引用实例对象在打印之后就被释放了,p被置为nil。那么底层做了什么?

2. 底层分析

我们先看汇编:

image-20220110165722806

这里可以看到弱引用调了swift_weakInit,我们进源码看一下:

image-20220110170112654

这里可以看到,声明一个weak变量相当于定义了一个WeakRefrence对象,我们接着看nativeInit

image-20220110170554621

image-20220110170616313

这里可以看到nativeInitrefCounts.formWeakReference()其实是创建了一个散列表(SideTable),那么散列表是如何创建的,我们接着看:

image-20220110171936989

这里可以看到看到,首先取出原先的refCounts,如果原先的refCounts有散列表则直接返回该散列表,如果原先的refCounts没有散列表或正在析构,那么直接返回nil。这里因为没有refCounts,所以创建了一个HeapObjectSideTableEntryside,然后调用了InlineRefCountBits(side)做了初始化操作,最后把side返回了。这里我们看一下HeapObjectSideTableEntry

image-20220110172533333

这里可以看到HeapObjectSideTableEntry存储了当前的实例对象objectrefCounts,注意这个refCounts的类型是SideTableRefCounts,与我们之前讲的InlineRefCounts有区别。这里我们查看一下SideTableRefCounts:

image-20220110174105015

这里可以看到SideTableRefCounts其实是模板类RefCountsInlineRefCounts也是如此),引用计数的具体类型取决于传进来的泛型参数SideTableRefCountBits。我们继续查看SideTableRefCountBits

image-20220110174505340

这里可以看到SideTableRefCountBits继承自RefCountBitsT,它除了继承了父类的64位的位域信息BitsType bits外自己还有个weakBits保存弱引用信息。我们在它父类中查看初始化方法:

image-20220110175617220

这里可以看到是在对传入的side的内存地址做位操作,查看源码(RefCount.h)可以知道SideTableUnusedLowBits为3,UseSlowRCShift为63,SideTableMarkShift为62。另外在搜索HeapObjectSideTableEntry的过程中,我们也可以发现源码中的注释:

image-20220110181511937

这里我们可以总结一下,本质上Swift中有2种引用计数:InlineRefCountsSideTableRefCounts,这2种引用计数共用的是模板类RefCountsstrong RC(强引用)unowned RC(无主引用)flags(标志位)用的是InlineRefCounts。弱引用用的是HeapObjectSideTableEntry(散列表)实例,这个HeapObjectSideTableEntry实例则包含了对象指针和SideTableRefCountsSideTableRefCounts的具体引用计数类型是SideTableRefCountBits(父类:RefCountBitsT)SideTableRefCountBits中存储了strong RC + unowned RC + weak RC + flags

3. 内存布局

这里我们先添加调试代码:

image-20220111094024132

这里可以看到弱引用修饰后,RefCounts变成了0xc00000002060fe80,把它复制到计算器中:

image-20220111094332054

这里注意一下,弱引用修饰后RefCounts的62位和63位变成了1。根据弱引用初始化方法中的位操作,我们反向操作还原它(62位63位还原,内存地址左移3位):

image-20220111095011753

这里还原后的地址应该就是散列表地址,我们看一下它的内存:

image-20220111101709808

这里可以很直观的看到散列表中的存储信息,就是我们之前说的HeapObjectSideTableEntry(散列表)实例包含了对象指针和SideTableRefCountsSideTableRefCounts中存储了strong RCweak RC

四、无主引用(unowned)

无主引用与弱引用有点类似,无主引用也不会对其引用的实例保持强引用。它们的区别是无主引用标记的属性或变量并不是可选类型,弱引用在引用对象释放后会将变量标记为nil,而无主引用并不会。无主引用假定引用对象是永远有值的,如果对象提前释放了,此时无主引用是危险的(野指针),所以它并不安全。根据苹果的官方文档的建议,当我们知道两个对象的生命周期并不相关,那么我们必须使用weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用unowned

1. 案例代码

这里我们添加调试代码:

image-20220111131132712

这里可以看到2次断点RefCounts的变化,初始化时是0x0000000000000002,unowned修饰后变成了0x0000000000000004。这个结果是如何得来的?

2. 底层分析

我们直接看汇编:

image-20220111131536866

这里可以看到unowned调了swift_unownedRetain,我们继续在在源码中看:

image-20220111131746659

这里可以看到调了refCounts.incrementUnowned(1),我们继续看incrementUnowned

image-20220111132039970

image-20220111132127123

这里可以看到,最终incrementUnownedRefCount是旧的无主引用计数取出来再加1。前面通过引用计数的内存布局我们知道RefCounts无主引用1~31位,这里我们先把0x0000000000000002复制到计算器:

image-20220111132614333

这里可以看到第1位为1,我们再加1:

image-20220111133022680

这样得到结果是0x4,也就是案例中的0x0000000000000004。同理,如果我们再增加一次unowned,结果应该是0x6。这里可以验证一下:

image-20220111133341307

image-20220111133415837

这里可以看到结果和预期一样的确是0x6

3. 应用场景

这里我们添加案例代码:

import Foundation

class Person {
    var name = "Tom"
    var age = 3
    var pet: Pet?

    deinit {
        print("Person deinit")
    }
}

class Pet {
    var name: String
    let owner: Person
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }

    deinit {
        print("Pet deinit")
    }
}

do {
    let p = Person()
    let dog = Pet(name: "Dog", owner: p)
    p.pet = dog
}

print("end")

/* 执行结果
end
Program ended with exit code: 0
*/

执行完之后发现,两个类的deinit都没有回调,这其实是一个典型的循环引用场景。这时我们对Pet稍做修改(给owner属性加关键字unowned):

class Pet {
    var name: String
    unowned let owner: Person
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }

    deinit {
        print("Pet deinit")
    }
}

/* 执行结果
Person deinit
Pet deinit
end
Program ended with exit code: 0
*/

重新执行发现两个类的deinit都能够正常回调了,也就是owner打破了循环引用。这里如果用weak也能达到相同的作用,为什么这里要用unowned。我们知道宠物类(Pet)类肯定是有一个主人(Person),也就是说Pet在销毁前Person必定是存在的(Person的生命周期比Pet生命周期长),按照苹果的设计原则,这里用unowned更适合。从本质上来说,unowned性能要比weak高,weak修饰的属性是可选类型,在取值时需要解包。当然,如果在开发过程中实在不清楚到底用哪个,稳妥起见还是用weak比较安全。这里我们可以总结一下:

  • 如果两个对象的生命周期完全没关系(其中一方任何时候赋值为nil,对对方都没影响,请用weak
  • 如果你的代码能确保:其中一个对象销毁,另一个对象也要跟着销毁,这时候可以(谨慎)用unowned