Swift進階-記憶體管理

本文的主要目的是探索 RefCount 的記憶體結構及強/弱引用計數管理

 

Swift 中也是採用 ARC 編譯器自動記憶體管理機制。

Swift 對象的記憶體結構是 HeapObject, 有兩個屬性 Metadata 和 RefCount , 各佔8位元組(64位)

RefCount 的每位的數據存儲內容如下圖所示:

 

1. 強引用的引用計數

1.1 數據結構

 

數據結構體大概是這樣:

struct InlineRefCountBits {
    var strongref: UInt32
    var unownedRef: UInt32
}

 

1.2 存儲方式

源碼中定義了 64 位的 refCount 聯合體的不同位置,存儲的不同含義:

 

 

我們通過下面這個小案例,來看看強引用的引用計數的存儲方式:

 

我們把這個值放到計算器中,對比著看下 64 位的二進位存儲情況:

 

33 – 62位存儲的是強引用數量,二進位的 11, 表示的是3,也就是有3個強引用計數強引用計數是從0開始的,t, t1, t2 三個變數指向創建的對象,所以對象的強引用計數為3。

那控制台里列印的0x0000000600000002中的6是怎麼回事呢?

  • 由於我們在控制台列印的是16進位,每4位為一組,第32 – 36位的 0110, 表示的是數字6,所以才會顯示成0x0000000600000002,這裡只是進位計算方式不同,並不是代表有6個引用計數。 
  • 由於二進位是從第33位開始存儲的值是11,16進位卻從32位開始存儲的值是110110 比 11向左移1位,所以就形成了2倍關係。所以我們從控制台列印出來的值,除以2,就是真實的強引用計數了

1.3 作用

ARC下,編譯器會根據強引用數量,來判斷一個對象是否應該被銷毀。如果強引用為0,就會銷毀這個對象

如果兩個對象,互相強持有對方,則會造成引用計數均不能為0,也就是無法銷毀,從而造成記憶體泄露的問題。

這時就需要弱引用或者無主引用,來將一個持有方式改為非強引用。這樣就可以解除循環引用的問題。

 

下面就是一個循環引用的例子:

class Teacher{
    var age = 18
    var closure:(()->())?
    
    deinit {
        print("Teacher 銷毀了")
    }
}

func test() {
    var t = Teacher()
    t.closure = {
        t.age = 10
    }
    print("方法執行完了")
}

test()

 

 

由於對象 t 持有closureclosure 內又捕獲了 t 的變數,從而強持有了 t . 所以造成了循環引用。

2.弱引用

使用weak 來修飾變數,這個變數就是弱引用,強引用計數不會再加1。weak 修飾後的變數,會變成一個可選項,也就是可以將 nil 賦值給它。

2.1 數據結構

數據結構:

 

調用流程:

 

所以WeakReference大概是這樣的結構:

struct WeakReference {
    var entry: HeapObjectSideTableEntry
}

struct HeapObjectSideTableEntry {
    var object: HeapObject
    var refCounts: SideTableRefCounts
}

struct SideTableRefCounts {
    var strongref: UInt32
    var unownedRef: UInt32
    var weakBits: UInt32
}

struct HeapObject {
    var kind: UnsafeRawPointer
    var strongref: UInt32
    var unownedRef: UInt32
}

 

2.2 存儲方式

 

下面是個例子:

 

 

我們創建了4個弱引用的變數,弱引用計數從1開始計數,所以是5. 

2.3 作用

weak 修飾的類型會自動變為可選類型,可以用於解除循環引用,在對象銷毀後,編譯器會自動置為nil。

1.3 中的例子,在捕獲 t 的時候,使用 weak 修飾一下,就不會形成閉包對 t 對象的強引用。

func test() {
    var t = Teacher()
    t.closure = {[weak t] in
        t?.age = 10
    }
    print("方法執行完了")
}

 

 

3 無主引用

存儲方式及數據結構在強引用中,已經分析過了,下面這個圖是結果:

 

3.1 計數方式

 

所以無主引用的計算方式為: 

  1. 轉換成 2 進位, 比如 6 轉成 110
  2. 右移一位,110 變 10, 也就等於10進位的3
  1. 減 1,3 – 1 = 2

對象 t 的無主引用的值是 2

 

3.2 作用

由於 weak 修飾後的變數是可選項,使用時需要解包,比較麻煩。 

如果能從上下文中確定變數的一定有值的話,可以使用 unowned 修飾變數來解除循環引用,因為它也不是強引用。這有點類似於隱式解包,在確定有值時再使用,如果對象釋放後,再執行這個閉包,會崩潰的。

 

4. 獲取引用計數

4.1 CFGetRetainCount

需要注意的是, 如果我們使用CFGetRetainCount來獲取強引用計數,這個方法的內部,會自動將當前的引用計數加1. 這一邏輯與 OC 是一致的:

4.2 swift_retainCount

從源碼看,swift_retainCount 獲取引用計數的時候,也是默認 + 1 的。

 

 

5.引用計數加1

在ARC下,編譯器為我們自動做了記憶體管理的操作,在有強引用 / 弱引用 / 無主引用指向對象時,都會執行對應的方法,去將對象對應的引用計數 + 1。

 

下面是swift_retain(強引用)的源碼:

 

6.引用計數減1

在ARC下,編譯器為我們自動做了記憶體管理的操作,當指向對象的變數離開當前作用域時,根據對應變數的類型(強引用 / 弱引用 / 無主引用),執行對於的方法,將對象對應的引用計數 – 1。

 

下面是swift_release(強引用)的源碼:

 

好了 給小夥伴分解到這裡就到此為止吧,以後會慢慢給大家講解說明。

青山不改,綠水長流,後會有期,感謝每一位佳人的支援!