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