Swift 中的記憶體管理詳解
這篇文章是在閱讀《The Swift Programming Language》Automatic Reference Counting(ARC,自動引用計數)一章時做的一些筆記,同時參考了其他的一些資料。
在早期的 iOS 開發中,記憶體管理是由開發者手動來完成的。因為傳統的垃圾回收機制對於移動平臺來說十分低效,蘋果採用的是引用計數(RC,Reference Counting)的方式來管理記憶體,開發者需要通過手工的方式增加或減少一個例項的引用計數。在 iOS 5 之後,引入了 ARC 自動引用計數,使得開發者不需要手動地呼叫 retain
和 release
來管理引用計數,但是實際上這些方法還是會被呼叫,只不過是交給了編譯器來完成,編譯器會在合適的地方幫我們加入這些方法。
什麼是自動引用計數?
每當你建立一個類的例項的時候,ARC 便會自動分配一塊記憶體空間來存放這個例項的資訊,當這個例項不再被使用的時候,ARC 便釋放例項所佔用的記憶體。一般每個被管理的例項都會與一個引用計數器相連,這個計數器儲存著當前例項被引用的次數,一旦建立一個新的引用指向這個例項,引用計數器便加 1,每當指向該例項的引用失效,引用計數器便減 1,當某個例項的引用計數器變成 0 的時候,這個例項就會被立即銷燬。
在 Swift 中,對引用描述的關鍵字有三個:strong
,weak
和 unowned
,所有的引用沒有特殊說明都是 strong
強引用型別。在 ARC 中,只有指向一個例項的所有 strong
強引用都斷開了,這個例項才會被銷燬。
舉一個簡單的例子:
class A { let name: String init(name: String) { self.name = name } deinit { print("A deinit") } } var a1: A? var a2: A? a1 = A(name: "A") a2 = a1 a1 = nil
上面這個例子中,雖然 a1
這個 strong
強引用斷開了,但是還有 a2
這個強引用指向這個例項,所以不會在命令列中輸出 A deinit
,當我們把 a2
也設定為 nil
時,與這個例項關聯的所有強引用均斷開了,這個例項便會被銷燬,在命令列中列印 A deinit
。
迴圈強引用(Strong Reference Cycles)
但是,在某些情況下,一個類例項的強引用數永遠不能變為 0,例如兩個類例項互相持有對方的強引用,因而每個類例項都讓對方一直存在,這就是所謂的強引用迴圈(Strong Reference Cycles)。
這裡引用 TSPL 中的例子:
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") } } class Apartment { let unit: String init(unit: String) { self.unit = unit } var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
每一個 Person
例項有一個可選的初始化為 nil
的 Apartment
型別,因為一個人並不總是擁有公寓。同樣,每一個 Apartment
例項都有一個可選的初始化為 nil
的 Person
型別,因為一個公寓並不總是屬於一個人。
接下來的程式碼片段定義了兩個可選型別的變數 john
和 unit4A
,並分別設定為下面的 Person
和 Apartment
的例項,這兩個變數都備受設定為 nil
:
var john: Person? var unit4A: Apartment?
現在可以建立特定的 Person
和 Apartment
例項,並將它們賦值給 john
和 unit4A
變數:
john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A")
下面一段程式碼將這兩個例項關聯起來:
john!.apartment = unit4A unit4A!.tenant = john
將兩個例項關聯在一起後,強引用的關係如圖所示:
這兩個例項關聯之後,會產生一個迴圈強引用,當斷開 john
和 unit4A
所持有的強引用時,引用計數器並不會歸零,所以這兩塊空間也得不到釋放,這就導致了記憶體洩漏。
可以將其中一個類中的變數設定為 weak
弱引用來打破這種強引用迴圈:
class Apartment { let unit: String init(unit: String) { self.unit = unit } weak var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
當斷開 john
和 unit4A
所持有的強引用時,Person instance
的引用計數器變成 0,例項被銷燬,從而 Apartment instance
的引用計數器也變為 0,例項被銷燬。
什麼時候使用 weak
?
當兩個例項是 optional 關聯在一起時,確保其中的一個使用 weak
弱引用,就像上面所說的那個例子一樣。
unowned
無主引用
在某些情況下,宣告的變數總是有值得時候,我們需要使用 unowned
無主引用。
同樣借用一下 TSPL 中的例子:
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class CreditCard { let number: UInt64 unowned let customer: Customer init(number: UInt64, customer: Customer) { self.number = number self.customer = customer } deinit { print("Card #\(number) is being deinitialized") } }
這裡定義了兩個類,Customer
和 CreditCard
,模擬了銀行客戶和客戶的信用卡,在這個例子中,每一個類都是將另一個類的例項作為自身的屬性,所以會產生迴圈強引用。
和之前那個例子不同的是,CreditCard
類中有一個非可選型別的 customer
屬性,因為,一個客戶可能有或者沒有一張信用卡,但是一張信用卡總是關聯著一個使用者。
var john: Customer? john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
關聯兩個例項後,它們的引用關係如圖所示:
當斷開 john
變數持有的強引用時,再也沒有指向 Customer
的強引用了,所以該例項被銷燬了,其後,再也沒有指向 CreditCard
的強引用了,該例項也被銷燬了。
什麼時候使用 unowned
無主引用?
兩個例項 A 和 B,如果例項 A 必須在例項 B 存在的前提下才能存在,那麼例項 A 必須用 unowned
無主引用指向例項 B。也就是說,有強制依賴性的那個例項必須對另一個例項持有無主引用。
例如上面那個例子所說,銀行客戶可能沒有信用卡,但是每張信用卡總是繫結著一個銀行客戶,所以信用卡這個類就需要用 unowned
無主引用。
無主引用以及隱市解析可選屬性
還有一種情況,兩個屬性都必須有值,並且初始化完成之後永遠不會為 nil
。在這種情況下,需要一個類使用 unowned
無主引用,另一個類使用隱式解析可選屬性。
閉包引起的迴圈強引用
在 Swift 中,閉包和函式都屬於引用型別。並且閉包還有一個特性:可以在其定義的上下文中捕獲常量或者變數。所以,在一個類中,閉包被賦值給了一個屬性,而這個閉包又使用了這個類的例項的時候,就會引起迴圈強引用。
Swift 提供了一種方法來解決這個問題:閉包捕獲列表(closure capture list)。在定義閉包的同時定義捕獲列表作為閉包的一部分,捕獲列表定義了閉包體內捕獲一個或者多個引用型別的規則。跟解決兩個類例項之間的迴圈強引用一樣,宣告每個捕獲的引用為弱引用或者無主引用。
捕獲列表中的每一項都由一對元素組成,一個元素是 weak
或者 unowned
關鍵字,另一個元素是類例項的引用(例如最常見得是 self
),這些在方括號內用逗號隔開。
具體的使用方法請參考官方文件。
何時使用 weak
,何時使用 unowned
在閉包和捕獲的例項總是相互引用並且總是同時銷燬的時候,將閉包內的捕獲定義為 unowned
無主引用。
在被捕獲的例項可能變成 nil
的情況下,使用 weak
弱引用。如果被捕獲的引用絕對不會變成 nil
,應該使用 unowned
無主引用,而不是 weak
弱引用。
Garbage Collection(GC,垃圾回收)
其實 ARC 應該也算 GC 的一種,不過我們一談到 GC,大多都會想到 Java 中的垃圾回收機制,相比較 GC,ARC 簡單得許多。以後有機會可以討論一下 Java 中的記憶體管理。
另外,需要注意的一點是,這裡所講的都是針對於引用型別
,結構體
和列舉
在 Swift 中屬於值型別,不在 ARC 的考慮範圍之內。
相關文章
- iOS記憶體管理詳解iOS記憶體
- 【記憶體管理】Oracle AMM自動記憶體管理詳解記憶體Oracle
- Java中的記憶體模型詳解Java記憶體模型
- [譯] Swift 中的記憶體洩漏Swift記憶體
- Swift 記憶體管理之 weak 與 unownedSwift記憶體
- javascript中的記憶體管理JavaScript記憶體
- 記憶體管理篇——實體記憶體的管理記憶體
- 詳解JVM中的記憶體模型是什麼?JVM記憶體模型
- 記憶體管理 記憶體管理概述記憶體
- JVM堆記憶體詳解JVM記憶體
- JVM記憶體模型詳解JVM記憶體模型
- Swift記憶體賦值探索一: 理解物件在記憶體中的儲存狀態Swift記憶體賦值物件
- Java的記憶體 -JVM 記憶體管理Java記憶體JVM
- Swift的ARC和記憶體洩漏Swift記憶體
- Java應用程式中的記憶體洩漏及記憶體管理Java記憶體
- 九、JVM記憶體模型詳解JVM記憶體模型
- Android 記憶體洩露詳解Android記憶體洩露
- 【記憶體管理】記憶體佈局記憶體
- Redis的記憶體回收機制和記憶體過期淘汰策略詳解Redis記憶體
- 「前端進階」JS中的記憶體管理前端JS記憶體
- python的記憶體管理Python記憶體
- CF的記憶體管理。記憶體
- JavaScript的記憶體管理JavaScript記憶體
- 記憶體管理兩部曲之實體記憶體管理記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- JVM之記憶體結構詳解JVM記憶體
- iOS Memory 記憶體詳解 (長文)iOS記憶體
- flink記憶體模型詳解與案例記憶體模型
- Redis 記憶體淘汰機制詳解Redis記憶體
- Go:記憶體管理與記憶體清理Go記憶體
- Swift列舉關聯值的記憶體探究Swift記憶體
- c 結構體記憶體對齊詳解結構體記憶體
- Swift 5強制獨佔記憶體Swift記憶體
- 筆記-更深層次的瞭解iOS記憶體管理筆記iOS記憶體
- JS中的棧記憶體、堆記憶體JS記憶體
- Objective-C中的記憶體管理機制Object記憶體
- 記憶體管理兩部曲之虛擬記憶體管理記憶體
- Linux共享記憶體的管理Linux記憶體
- C++六種記憶體序詳解C++記憶體