Swift內(nèi)存管理

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

那么為什么其 refCounts0x0000000600000003 呢?

在前面分析swift的類結(jié)構(gòu)時(shí),通過(guò)SIL查看源碼:

refCountsInlineRefCounts 類型的。

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)注 UnownedRefCountStrongExtraRefCount ,將剛剛例子中的 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)初始化捕獲列表中定義的常量。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,489評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,290評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,776評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,510評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,866評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,036評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,585評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,331評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,536評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,754評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,154評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,469評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,273評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,505評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容