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位開始存儲的值是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(強引用)的源碼:
好了 給小夥伴分解到這裡就到此為止吧,以後會慢慢給大家講解說明。
青山不改,綠水長流,後會有期,感謝每一位佳人的支援!