Swift
中使用自動(dòng)引用計(jì)數(shù)(ARC)
機(jī)制來(lái)追蹤和管理內(nèi)存。
-
強(qiáng)引用
class YYTeacher {
var age : Int = 18
var name : String = "YY"
}
var t = YYTeacher()
var t1 = t
var t2 = t
通過(guò) lldb
端可知上述代碼執(zhí)行完成后, t
的內(nèi)存情況如下:
那么為什么其 refCounts
為 0x0000000600000003
呢?
在前面分析swift的類結(jié)構(gòu)時(shí),通過(guò)SIL查看源碼:
refCounts
是 InlineRefCounts
類型的。
而 InlineRefCounts
又是 RefCounts
這個(gè)模板類的別名,這個(gè)模板類又取決于其傳入的參數(shù)類型 InlineRefCountBits
。
InlineRefCountBits
又是 RefCountBitsT
這個(gè)模板類的別名。
RefCountBitsT
這個(gè)類中有一個(gè)屬性 BitsType
類型的 bits
,通過(guò)查看定義可知:
BitsType
其實(shí)是 RefCountBitsInt
這個(gè)結(jié)構(gòu)體中 Type
屬性的別名,所以 bits
其實(shí)就是 uint64_t
類型。
分析了 RefCountBitsT
這個(gè)類中的屬性bits
,再來(lái)分析一下 swift
中的創(chuàng)建對(duì)象的底層方法 _swift_allocObject_
:
查看 Initialized
的定義可知是一個(gè)枚舉
:
refCounts
對(duì)應(yīng)的構(gòu)造函數(shù):
可知在這里真正做事的是 RefCountBits
:
點(diǎn)進(jìn)去可知 RefCountBits
也是一個(gè)模板定義,所以真正的初始化操作應(yīng)該是在 RefCountBitsT
這個(gè)類中的構(gòu)造方法:
根據(jù) offset
進(jìn)行的一個(gè)位移
操作。
分析 RefCountBitsT
的結(jié)構(gòu),如下圖:
-
isImmortal
(0) -
UnownedRefCount
(1-31):無(wú)主引用計(jì)數(shù) -
isDeinitingMask
(32):是否釋放的標(biāo)記 -
StrongExtraRefCount
(33-62):強(qiáng)引用計(jì)數(shù) -
UseSlowRC
(63)
這里重點(diǎn)關(guān)注 UnownedRefCount
和 StrongExtraRefCount
,將剛剛例子中的 t
的引用計(jì)數(shù)0x0000000600000003
對(duì)比:
可知例子中代碼執(zhí)行完后,t
的強(qiáng)引用計(jì)數(shù)
變成了3.
通過(guò)分析例子中的SIL文件:
通過(guò)查看SIL文檔
可知 copy_addr
內(nèi)部又調(diào)用了 一次strong_retain
,而 strong_retain
其實(shí)就是 swift_retain
從上圖可知最終調(diào)用的就是 __swift_retain_
這個(gè)方法,再往下走:
綜合上述可知:
__swift_retain_
其實(shí)就是強(qiáng)引用計(jì)數(shù)
增加了1
。
注意:如果使用 CFGetRetainCount(t)
來(lái)獲取 t
的強(qiáng)引用計(jì)數(shù), t
的強(qiáng)引用計(jì)數(shù)會(huì)在原來(lái)的基礎(chǔ)上 +1
。
-
弱引用
使用關(guān)鍵字weak
聲明弱引用
的變量,
weak var t = YYTeacher()
從上圖可知弱引用
聲明的變量是一個(gè)可選值
,因?yàn)樵诔绦蜻\(yùn)行過(guò)程中是允許將當(dāng)前變量設(shè)置為 nil
的。如下面的例子:
class YYTeacher {
var age : Int = 18
var name : String = "YY"
deinit {
print("YYTeacher deinit")
}
}
var t = YYTeacher()
t = nil
上面的代碼在t = nil
處會(huì)報(bào)錯(cuò)
,可是如果將變量 t
聲明成可選類型,再將 t = nil
則不會(huì)報(bào)錯(cuò),所以意味著 weak
聲明的變量必須是一個(gè)可選類型
,才能被允許被設(shè)置為 nil
。
通過(guò)源碼
分析 weak
關(guān)鍵字在底層到底做了什么?
class YYTeacher {
var age : Int = 18
var name : String = "YY"
}
var t = YYTeacher()
weak var t1 = t
匯編模式下可知調(diào)用了 swift_weakInit
這個(gè)方法
通過(guò) SIL
中查看源碼看一下 swift_weakInit()
這個(gè)方法究竟做了什么?
到這里,就引出了 HeapObjectSideTableEntry
這個(gè)類
SideTableRefCountBits
決定了SideTableRefCounts
的真實(shí)類型,繼承自 RefCountBitsT
,多了一個(gè) uint32_t
的屬性 weakBits
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
uint32_t weakBits;
// ...
}
綜上可知:HeapObjectSideTableEntry
其實(shí)就是原來(lái)的 uint64_t
再加上一個(gè)存儲(chǔ)弱引用計(jì)數(shù)的 uint32_t
。
了解 HeapObjectSideTableEntry
后,接著看 allocateSideTable()
方法中的實(shí)現(xiàn),在新建了一個(gè) SideTable
對(duì)象后,將其作為參數(shù)傳入調(diào)用了 InlineRefCountBits()
這個(gè)構(gòu)造方法:
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);
// ...
前面學(xué)習(xí)了 HeapObject
里的 RefCounts
實(shí)際上是InlineRefCountBits
的一個(gè)模板參數(shù),在這里構(gòu)造完 Side Table
后,對(duì)象中 InlineRefCountBits
就不再是原來(lái)的引用計(jì)數(shù)了,而是一個(gè)指向 Side Table
的指針,因?yàn)樗鼈兌际?uint64_t
,所以需要不同的構(gòu)造函數(shù)來(lái)區(qū)分,這里 InlineRefCountBits
的構(gòu)造函數(shù)如下:
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
其實(shí)就是把 Side Table
的地址做了一個(gè)偏移
存放到內(nèi)存中(即把創(chuàng)建的 side
的地址
存放到 uint64_t
這個(gè)變量中),將指針地址沒(méi)用的位置替換成標(biāo)識(shí)位
。
擴(kuò)展:如果此時(shí)再增加引用計(jì)數(shù)會(huì)怎樣呢?
通過(guò)前面可知:這里在給 t2
賦值的時(shí)候,內(nèi)部會(huì)調(diào)用
strong_retain
--> swift_retain
--> __swift_retain
void incrementStrong(uint32_t inc) {
refCounts.increment(inc);
}
通過(guò)查看源碼可知此時(shí)的 refCounts
其實(shí)就是 SideTableRefCounts
,所以這個(gè)時(shí)候其實(shí)操作的就是SideTable
。
總結(jié):上面講了兩種 RefCounts
,
InlineRefCounts
:用在HeapObjet
中,其實(shí)就是一個(gè)uint64_t
,沒(méi)有弱引用時(shí)存的是引用計(jì)數(shù)
(strongRefCounts + unownedRefCounts),有弱引用時(shí)存的是Side Table
的指針地址HeapObjectSideTableEntry
:內(nèi)部有一個(gè)實(shí)質(zhì)為SideTableRefCountBits
類型的屬性refCounts
,該類型中多了一個(gè)屬性uint32_t weakBits
,繼承自RefCountBitsT
,即原來(lái)的uint64_t
加上一個(gè)存儲(chǔ)弱引用
計(jì)數(shù)的uint32_t
。
Side Table
的指針地址中存儲(chǔ)的是:object
+?
+引用計(jì)數(shù)(strongRefCounts + unownedRefCounts)
+弱引用計(jì)數(shù)(weakRefCounts)
-
循環(huán)引用
var age = 10
let closure = {
age += 1
}
closure()
上面例子中,執(zhí)行完閉包后,age的值為11,說(shuō)明閉包
內(nèi)部對(duì)變量的修改
將會(huì)改變
外部原始變量的值,原因是閉包
能默認(rèn)捕獲外部變量
,和OC
中的Block
是一樣的。
接下來(lái)通過(guò)deinit
來(lái)觀察實(shí)例對(duì)象是否將被釋放:
deinit
:在swift中叫做反初始化器
,實(shí)例變量將要被回收
時(shí)調(diào)用deinit
,觀察實(shí)例對(duì)象是否被銷毀。
class YYTeacher {
var age = 12
deinit {
print("YYTeacher deinit")
}
}
func test() {
let t = YYTeacher()
let closure = {
t.age += 10
}
closure()
print(t.age)
}
test()
在上面例子中,當(dāng)執(zhí)行完函數(shù)test
后,實(shí)例對(duì)象t
調(diào)用了deinit
即將被銷毀,說(shuō)明閉包內(nèi)部調(diào)用外部變量不
會(huì)對(duì)其做強(qiáng)引用
操作。
那么怎樣才會(huì)造成循環(huán)引用
呢?
class YYTeacher {
var age = 12
var completionBack : (()->())?
deinit {
print("YYTeacher deinit")
}
}
func test() {
let t = YYTeacher()
t.completionBack = {
t.age += 10
}
t.completionBack!()
}
test()
執(zhí)行完上面的代碼會(huì)發(fā)現(xiàn),并沒(méi)有
調(diào)用deinit
,說(shuō)明這里有循環(huán)引用
,導(dǎo)致實(shí)例對(duì)象不
能被銷毀
。
在Swift
中,通過(guò)弱引用(weak)
和無(wú)主引用(unowned)
來(lái)解決循環(huán)引用。
weak
:可選類型
,實(shí)例的生命周期內(nèi)可為nil
,實(shí)例銷毀后實(shí)例對(duì)象被置為nil;
unowned
:非可選類型
,使用前提要確保實(shí)例對(duì)象初始化后永不
能為nil
,實(shí)例銷毀后仍存儲(chǔ)著實(shí)例對(duì)象的內(nèi)存地址
,若再訪問(wèn)則會(huì)造成野指針
錯(cuò)誤。
這里解決循環(huán)引用:
t.completionBack = {[unowned t] in
t.age += 10
}
// 或者
t.completionBack = {[weak t] in
t?.age += 10
}
/** 這里weak t是可選類型,可能為nil,OC中是允許給nil對(duì)象發(fā)送消息的,而在Swift中是不允許給nil對(duì)象發(fā)送消息的,所以要加上?,如果為nil則不會(huì)執(zhí)行后面的.age+=1*/
上面的語(yǔ)法在Swift也叫做捕獲列表
,定義在參數(shù)列表之前,用[ ]
括起來(lái),若有多個(gè),中間用,
連接,即使省略參數(shù)名稱、參數(shù)類型和返回類型也必須加上關(guān)鍵字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
上面例子中,三個(gè)閉包捕獲的都是最后一次i
的值,如果想要打印出來(lái)的值為1、2、3,三個(gè)閉包捕獲的就應(yīng)該是每次i的值的副本(copy)
,即捕獲列表
,閉包表達(dá)改為:
arrClosure.append {[i] in
print(i)
}
總結(jié):捕獲列表中捕獲的是變量的一個(gè)副本
,本地表格
;對(duì)于捕獲列表中的每個(gè)常量,閉包會(huì)利用周圍范圍內(nèi)具有相同名稱的常量或變量,來(lái)初始化捕獲列表中定義的常量。