本文的主要目的是探索 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位開始存儲的值是110。110 比 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 持有closure,closure 內又捕獲了 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 計數方式
所以無主引用的計算方式為:
- 轉換成 2 進制, 比如 6 轉成 110
- 右移一位,110 變 10, 也就等於10進制的3
- 減 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(強引用)的源碼:
好了 給小伙伴分解到這里就到此為止吧,以后會慢慢給大家講解說明。
青山不改,綠水長流,后會有期,感謝每一位佳人的支持!