一、内存管理
我们都知道iOS的内存管理是基于引用计数,那么在底层它是如何管理的呢?
1. 案例代码
之前在分析类的初始化流程中,我们知道Swift源码中对象的初始化方法中最后返回了HeapObject对象。这个 HeapObject有两个属性:metadata和refCounts。前面8字节是metadata,后面8字节是refCounts(红框部分),那么refCounts的0x0000000000000002是如何来的呢?我们首先分析一下引用计数的存储方式:
2. 引用计数的存储方式
首先我们定位到HeapObject的定义:
这里可以看到HeapObject有两个属性:metadata和refCounts。这个refCounts是InlineRefCounts类型。我们进一步查看InlineRefCounts:
这里可以看到InlineRefCounts其实是一个模板类RefCounts,它有一个泛型参数InlineRefCountBits,我们先看下RefCounts:
这里可以看到RefCounts仅仅是对InlineRefCounts一个包装,而引用计数的具体类型取决于传进来的泛型参数RefCountBits也是前面的InlineRefCountBits。我们继续查看InlineRefCountBits:
这里可以看到InlineRefCountBits也是一个模板类RefCountBitsT,它有一个泛型参数RefCountIsInline,这个泛型参数的定义是enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };,也就是说它传入的其实是Bool值。我们接着看RefCountBitsT:
这里可以看到RefCountBitsT有一个属性bits,我们查看bits:
这里可以看到Type其实是一个64位的位域信息(与OC一样)。由此我们可以得出一个结论,Swift中引用计数管理类是RefCountBitsT,它有一个64位的属性bits用来存储一个对象运行生命周期相关的引用计数。这一点弄清楚之后我们再分析一下案例中refCounts的0x0000000000000002的由来:
3. 引用计数的计算方式
首先我们看一下对象的创建:
之前在分析类的初始化流程中我们知道类的创建最终会到_swift_allocObject_中,在这里会调初始化方法new (object) HeapObject(metadata);,继续查看这个初始化方法:
这里可以看到初始化赋值了Initialized,查看这个Initialized:
这里可以看到这个Initialized其实是枚举类型Initialized_t,它最终传的是refCounts(RefCountBits(0, 1)),而这个RefCountBits其实就是我们之前讲的RefCountBitsT,查看RefCountBitsT的初始化方法:
这里可以看到初始化方法是对传进的两个参数strongExtraCount,unownedCount做位操作。查看源码(RefCount.h)可以得知,StrongExtraRefCountShift为33,UnownedRefCountShift为1。带入计算可以得到2,这就是案例中refCounts为0x0000000000000002的原因。
4. 引用计数的内存布局
引用计数的位域信息如上图所示,isImmortal和UseSlowRC分别占据第0和第63位;无主引用在1~31位;强引用在33~62位;正在释放在第32位。这里我们验证一下正在释放的对象引用计数:
这里断点调试可以看到当p = nil后,引用计数是0x0000000100000002,复制到计算器中查看:
这里可以看到第32位是1表示正在销毁。
二、强引用(strong)
Swift中对象在赋值时默认就是强引用。
1. 案例代码
这里我们添加调试代码:
2. 底层分析
断点进汇编:
这里可以看到,强引用会调swift_retain,我们再进源码看一下:
最后调到源码的_swift_retain_,这里调了refCounts.increment(1),我们接着看increment方法:
increment可以看到调了incrementStrongExtraRefCount,我们再看incrementStrongExtraRefCount:
这里可以看到强引用在做位运算,原引用计数加1后左移33位。
三、弱引用(weak)
弱引用不会对其引用的实例保持强引用,因而不会阻止ARC 释放被引用的实例,这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上 关键字表明这是一个弱引用。由于弱引用不会强保持对实例的引用,所以说实例被释放了弱引用仍旧引用着这个实例也是有可能的。因此,ARC 会在被引用的实例被释放是自动设置弱引用为 nil 。由于弱引用需要允许它们的值为 nil , 它们一定得是可选类型。
1. 案例代码
这里我们先添加案例代码:
这里可以看出弱引用实例对象在打印之后就被释放了,p被置为nil。那么底层做了什么?
2. 底层分析
我们先看汇编:
这里可以看到弱引用调了swift_weakInit,我们进源码看一下:
这里可以看到,声明一个weak变量相当于定义了一个WeakRefrence对象,我们接着看nativeInit:
这里可以看到nativeInit中refCounts.formWeakReference()其实是创建了一个散列表(SideTable),那么散列表是如何创建的,我们接着看:
这里可以看到看到,首先取出原先的refCounts,如果原先的refCounts有散列表则直接返回该散列表,如果原先的refCounts没有散列表或正在析构,那么直接返回nil。这里因为没有refCounts,所以创建了一个HeapObjectSideTableEntry的side,然后调用了InlineRefCountBits(side)做了初始化操作,最后把side返回了。这里我们看一下HeapObjectSideTableEntry:
这里可以看到HeapObjectSideTableEntry存储了当前的实例对象object和refCounts,注意这个refCounts的类型是SideTableRefCounts,与我们之前讲的InlineRefCounts有区别。这里我们查看一下SideTableRefCounts:
这里可以看到SideTableRefCounts其实是模板类RefCounts(InlineRefCounts也是如此),引用计数的具体类型取决于传进来的泛型参数SideTableRefCountBits。我们继续查看SideTableRefCountBits:
这里可以看到SideTableRefCountBits继承自RefCountBitsT,它除了继承了父类的64位的位域信息BitsType bits外自己还有个weakBits保存弱引用信息。我们在它父类中查看初始化方法:
这里可以看到是在对传入的side的内存地址做位操作,查看源码(RefCount.h)可以知道SideTableUnusedLowBits为3,UseSlowRCShift为63,SideTableMarkShift为62。另外在搜索HeapObjectSideTableEntry的过程中,我们也可以发现源码中的注释:
这里我们可以总结一下,本质上Swift中有2种引用计数:InlineRefCounts和SideTableRefCounts,这2种引用计数共用的是模板类RefCounts。strong RC(强引用)、unowned RC(无主引用)和flags(标志位)用的是InlineRefCounts。弱引用用的是HeapObjectSideTableEntry(散列表)实例,这个HeapObjectSideTableEntry实例则包含了对象指针和SideTableRefCounts,SideTableRefCounts的具体引用计数类型是SideTableRefCountBits(父类:RefCountBitsT),SideTableRefCountBits中存储了strong RC + unowned RC + weak RC + flags。
3. 内存布局
这里我们先添加调试代码:
这里可以看到弱引用修饰后,RefCounts变成了0xc00000002060fe80,把它复制到计算器中:
这里注意一下,弱引用修饰后RefCounts的62位和63位变成了1。根据弱引用初始化方法中的位操作,我们反向操作还原它(62位63位还原,内存地址左移3位):
这里还原后的地址应该就是散列表地址,我们看一下它的内存:
这里可以很直观的看到散列表中的存储信息,就是我们之前说的HeapObjectSideTableEntry(散列表)实例包含了对象指针和SideTableRefCounts,SideTableRefCounts中存储了strong RC 和 weak RC 。
四、无主引用(unowned)
无主引用与弱引用有点类似,无主引用也不会对其引用的实例保持强引用。它们的区别是无主引用标记的属性或变量并不是可选类型,弱引用在引用对象释放后会将变量标记为nil,而无主引用并不会。无主引用假定引用对象是永远有值的,如果对象提前释放了,此时无主引用是危险的(野指针),所以它并不安全。根据苹果的官方文档的建议,当我们知道两个对象的生命周期并不相关,那么我们必须使用weak。相反,非强引用对象拥有和强引用对象同样或者更长的生命周期的话,则应该使用unowned。
1. 案例代码
这里我们添加调试代码:
这里可以看到2次断点RefCounts的变化,初始化时是0x0000000000000002,unowned修饰后变成了0x0000000000000004。这个结果是如何得来的?
2. 底层分析
我们直接看汇编:
这里可以看到unowned调了swift_unownedRetain,我们继续在在源码中看:
这里可以看到调了refCounts.incrementUnowned(1),我们继续看incrementUnowned:
这里可以看到,最终incrementUnownedRefCount是旧的无主引用计数取出来再加1。前面通过引用计数的内存布局我们知道RefCounts中无主引用在1~31位,这里我们先把0x0000000000000002复制到计算器:
这里可以看到第1位为1,我们再加1:
这样得到结果是0x4,也就是案例中的0x0000000000000004。同理,如果我们再增加一次unowned,结果应该是0x6。这里可以验证一下:
这里可以看到结果和预期一样的确是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。