Swift
中使用自動引用計數(shù)(ARC)
機制來追蹤和管理內存酗昼。
-
強引用
class YYTeacher {
var age : Int = 18
var name : String = "YY"
}
var t = YYTeacher()
var t1 = t
var t2 = t
通過 lldb
端可知上述代碼執(zhí)行完成后, t
的內存情況如下:
那么為什么其 refCounts
為 0x0000000600000003
呢梳猪?
在前面分析swift的類結構時麻削,通過SIL查看源碼:
refCounts
是 InlineRefCounts
類型的。
而 InlineRefCounts
又是 RefCounts
這個模板類的別名春弥,這個模板類又取決于其傳入的參數(shù)類型 InlineRefCountBits
呛哟。
InlineRefCountBits
又是 RefCountBitsT
這個模板類的別名。
RefCountBitsT
這個類中有一個屬性 BitsType
類型的 bits
匿沛,通過查看定義可知:
BitsType
其實是 RefCountBitsInt
這個結構體中 Type
屬性的別名扫责,所以 bits
其實就是 uint64_t
類型。
分析了 RefCountBitsT
這個類中的屬性bits
,再來分析一下 swift
中的創(chuàng)建對象的底層方法 _swift_allocObject_
:
查看 Initialized
的定義可知是一個枚舉
:
refCounts
對應的構造函數(shù):
可知在這里真正做事的是 RefCountBits
:
點進去可知 RefCountBits
也是一個模板定義淮逻,所以真正的初始化操作應該是在 RefCountBitsT
這個類中的構造方法:
根據 offset
進行的一個位移
操作实抡。
分析 RefCountBitsT
的結構,如下圖:
-
isImmortal
(0) -
UnownedRefCount
(1-31):無主引用計數(shù) -
isDeinitingMask
(32):是否釋放的標記 -
StrongExtraRefCount
(33-62):強引用計數(shù) -
UseSlowRC
(63)
這里重點關注 UnownedRefCount
和 StrongExtraRefCount
苏揣,將剛剛例子中的 t
的引用計數(shù)0x0000000600000003
對比:
可知例子中代碼執(zhí)行完后,t
的強引用計數(shù)
變成了3.
通過分析例子中的SIL文件:
通過查看SIL文檔
可知 copy_addr
內部又調用了 一次strong_retain
推姻,而 strong_retain
其實就是 swift_retain
從上圖可知最終調用的就是 __swift_retain_
這個方法平匈,再往下走:
綜合上述可知:
__swift_retain_
其實就是強引用計數(shù)
增加了1
。
注意:如果使用 CFGetRetainCount(t)
來獲取 t
的強引用計數(shù)藏古, t
的強引用計數(shù)會在原來的基礎上 +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
再加上一個存儲弱引用計數(shù)的 uint32_t
裙犹。
了解 HeapObjectSideTableEntry
后尽狠,接著看 allocateSideTable()
方法中的實現(xiàn)衔憨,在新建了一個 SideTable
對象后,將其作為參數(shù)傳入調用了 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
的一個模板參數(shù)袄膏,在這里構造完 Side Table
后践图,對象中 InlineRefCountBits
就不再是原來的引用計數(shù)了,而是一個指向 Side Table
的指針沉馆,因為它們都是 uint64_t
码党,所以需要不同的構造函數(shù)來區(qū)分,這里 InlineRefCountBits
的構造函數(shù)如下:
RefCountBitsT(HeapObjectSideTableEntry* side)
: bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
| (BitsType(1) << Offsets::UseSlowRCShift)
| (BitsType(1) << Offsets::SideTableMarkShift))
{
assert(refcountIsInline);
}
其實就是把 Side Table
的地址做了一個偏移
存放到內存中(即把創(chuàng)建的 side
的地址
存放到 uint64_t
這個變量中)斥黑,將指針地址沒用的位置替換成標識位
揖盘。
擴展:如果此時再增加引用計數(shù)會怎樣呢?
通過前面可知:這里在給 t2
賦值的時候锌奴,內部會調用
strong_retain
--> swift_retain
--> __swift_retain
void incrementStrong(uint32_t inc) {
refCounts.increment(inc);
}
通過查看源碼可知此時的 refCounts
其實就是 SideTableRefCounts
兽狭,所以這個時候其實操作的就是SideTable
。
總結:上面講了兩種 RefCounts
鹿蜀,
InlineRefCounts
:用在HeapObjet
中箕慧,其實就是一個uint64_t
,沒有弱引用時存的是引用計數(shù)
(strongRefCounts + unownedRefCounts)茴恰,有弱引用時存的是Side Table
的指針地址HeapObjectSideTableEntry
:內部有一個實質為SideTableRefCountBits
類型的屬性refCounts
颠焦,該類型中多了一個屬性uint32_t weakBits
,繼承自RefCountBitsT
琐簇,即原來的uint64_t
加上一個存儲弱引用
計數(shù)的uint32_t
蒸健。
Side Table
的指針地址中存儲的是:object
+?
+引用計數(shù)(strongRefCounts + unownedRefCounts)
+弱引用計數(shù)(weakRefCounts)
-
循環(huán)引用
var age = 10
let closure = {
age += 1
}
closure()
上面例子中,執(zhí)行完閉包后婉商,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()
在上面例子中箫攀,當執(zhí)行完函數(shù)test
后肠牲,實例對象t
調用了deinit
即將被銷毀,說明閉包內部調用外部變量不
會對其做強引用
操作靴跛。
那么怎樣才會造成循環(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í)行完上面的代碼會發(fā)現(xiàn),并沒有
調用deinit
梢睛,說明這里有循環(huán)引用
肥印,導致實例對象不
能被銷毀
识椰。
在Swift
中,通過弱引用(weak)
和無主引用(unowned)
來解決循環(huán)引用深碱。
weak
:可選類型
腹鹉,實例的生命周期內可為nil
,實例銷毀后實例對象被置為nil敷硅;
unowned
:非可選類型
功咒,使用前提要確保實例對象初始化后永不
能為nil
,實例銷毀后仍存儲著實例對象的內存地址
绞蹦,若再訪問則會造成野指針
錯誤力奋。
這里解決循環(huán)引用:
t.completionBack = {[unowned t] in
t.age += 10
}
// 或者
t.completionBack = {[weak t] in
t?.age += 10
}
/** 這里weak t是可選類型,可能為nil坦辟,OC中是允許給nil對象發(fā)送消息的刊侯,而在Swift中是不允許給nil對象發(fā)送消息的,所以要加上锉走?滨彻,如果為nil則不會執(zhí)行后面的.age+=1*/
上面的語法在Swift也叫做捕獲列表
,定義在參數(shù)列表之前挪蹭,用[ ]
括起來亭饵,若有多個,中間用,
連接梁厉,即使省略參數(shù)名稱辜羊、參數(shù)類型和返回類型也必須加上關鍵字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)
}
總結:捕獲列表中捕獲的是變量的一個副本
骤肛,本地表格
;對于捕獲列表中的每個常量窍蓝,閉包會利用周圍范圍內具有相同名稱的常量或變量腋颠,來初始化捕獲列表中定義的常量。