Swift內存管理

Swift 中使用自動引用計數(ARC)機制來追蹤和管理內存。

  • 強引用

class YYTeacher {
    var age : Int = 18
    var name : String = "YY"
}

var t = YYTeacher()
var t1 = t
var t2 = t

通過 lldb 端可知上述代碼執行完成后, t 的內存情況如下:

那么為什么其 refCounts0x0000000600000003 呢?

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

refCountsInlineRefCounts 類型的。

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)

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容