Swift內存管理

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 的內存情況如下:

那么為什么其 refCounts0x0000000600000003 呢梳猪?

在前面分析swift的類結構時麻削,通過SIL查看源碼:

refCountsInlineRefCounts 類型的。

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)

這里重點關注 UnownedRefCountStrongExtraRefCount 苏揣,將剛剛例子中的 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)
}

總結:捕獲列表中捕獲的是變量的一個副本骤肛,本地表格;對于捕獲列表中的每個常量窍蓝,閉包會利用周圍范圍內具有相同名稱的常量或變量腋颠,來初始化捕獲列表中定義的常量。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末吓笙,一起剝皮案震驚了整個濱河市淑玫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖混移,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祠墅,死亡現(xiàn)場離奇詭異,居然都是意外死亡歌径,警方通過查閱死者的電腦和手機毁嗦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來回铛,“玉大人狗准,你說我怎么就攤上這事∫鹚啵” “怎么了腔长?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長验残。 經常有香客問我捞附,道長,這世上最難降的妖魔是什么您没? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任鸟召,我火速辦了婚禮,結果婚禮上氨鹏,老公的妹妹穿的比我還像新娘欧募。我一直安慰自己,他們只是感情好仆抵,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布跟继。 她就那樣靜靜地躺著,像睡著了一般镣丑。 火紅的嫁衣襯著肌膚如雪舔糖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天莺匠,我揣著相機與錄音剩盒,去河邊找鬼。 笑死慨蛙,一個胖子當著我的面吹牛,可吹牛的內容都是我干的纪挎。 我是一名探鬼主播期贫,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼异袄!你這毒婦竟也來了通砍?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎封孙,沒想到半個月后迹冤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡虎忌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年泡徙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膜蠢。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡堪藐,死狀恐怖,靈堂內的尸體忽然破棺而出挑围,到底是詐尸還是另有隱情礁竞,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布杉辙,位于F島的核電站模捂,受9級特大地震影響,放射性物質發(fā)生泄漏蜘矢。R本人自食惡果不足惜狂男,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望硼端。 院中可真熱鬧并淋,春花似錦、人聲如沸珍昨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽镣典。三九已至兔毙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間兄春,已是汗流浹背澎剥。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赶舆,地道東北人哑姚。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像芜茵,于是被迫代替她去往敵國和親叙量。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容