Swift 中使用自动引用计数(ARC)机制来追踪和管理内存。
-
强引用
class YYTeacher {
var age : Int = 18
var name : String = "YY"
}
var t = YYTeacher()
var t1 = t
var t2 = t
通过 lldb 端可知上述代码执行完成后, t 的内存情况如下:

那么为什么其 refCounts 为 0x0000000600000003 呢?
在前面分析swift的类结构时,通过SIL查看源码:


refCounts 是 InlineRefCounts 类型的。


而 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)
这里重点关注 UnownedRefCount 和 StrongExtraRefCount ,将刚刚例子中的 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)
}
总结:捕获列表中捕获的是变量的一个副本,本地表格;对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。
