編寫代碼時需注意是否產生了循環引用,因此就產生了什麼時候使用weak
、unowned
問題?這篇文章將介紹 Swift 中的strong
、weak
、unowned
的區別。
1. ARC
自動引用計數(即 Automated Reference Count,簡稱 ARC)是 Xcode 4.2版本的新特性,其與手動管理內存使用了相同的計數系統。不同點在於:系統在編譯時會幫助我們插入合適的內存管理方法,保留和釋放都會自動進行,避免了手動管理引用計數的一些潛在問題。
Swift 使用自動引用計數跟蹤、管理app的內存。通常情況下,這意味着ARC會自動管理內存,開發者無需關注內存管理。當類的實例不再使用時,ARC會自動釋放其佔用的內存。
爲幫助管理內存,ARC 有時需瞭解類之間的關係。在 Swift 中使用 ARC 與在 Objective-C 中使用 ARC 類似。
引用計數只適用類的實例。結構體和枚舉是值類型,不是引用類型,存儲和傳遞的時候並非使用引用。
2. strong
strong
指針通過增加指向對象的引用計數,保護被指向對象不被ARC釋放。即,只要有一個強指針指向該對象,它就不會被釋放。
Swift 中聲明的屬性默認是strong
。當對象間引用關係是線性時,使用strong
指針不會產生問題。
當兩個實例使用強指針指向彼此時,兩個實例引用計數都不會變爲零,即產生循環引用(strong reference cycle)。
下面是一個循環引用的示例:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\(name) is being deinitizlized")
}
}
class Apartment {
let unit: String
init(unit: String) {
self.unit = unit
}
var tenant: Person?
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
上面定義了兩個類:Person
和Apartment
,代表住戶和公寓。兩個類都實現了deinitializer
方法,當類的實例銷燬時進行打印,方便觀察實例佔用的內存是否釋放了。
下面代碼定義了兩個可選類型的變量,初始值爲nil。併爲其分配兩個新的實例:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
目前,john
變量強引用Person
實例,unit4A
變量強引用Apartment
實例,如下所示:
現在連接兩個實例。person
持有apartment
,apartment
持有person
。
john?.apartment = unit4A
unit4A?.tenant = John
連接兩個實例後,引用關係如下:
Person
的實例強引用了Apartment
的實例,Apartment
的實例強引用了Person
的實例,即產生了循環引用。當移除john
、unit4A
的引用時,實例的引用計數不會變爲零,也就是實例內存不會被ARC釋放。
john = nil
unit4A = nil
設置john
、unit4A
變量爲nil
後,引用關係如下:
Person
和Apartment
實例之間的強引用無法破除。
3. 解決循環引用
Swift 提供了兩種解決循環引用的方案:weak
和unowned
。weak
和unowned
引用其它實例時不會產生強引用,引用計數不會加一。因此,不會產生循環引用。
當一個實例的生命週期短於另一個時(即一個實例可以先被銷燬),使用weak
引用。在上面公寓的示例中可能出現公寓沒有住戶的情況。因此,可以使用weak
解決循環引用問題。當另一個實例生命週期與當前實例相同,或長於當前實例時,使用unowned
引用。
3.1 weak引用
weak
引用不會強持有引用的實例,也就不會阻止ARC釋放實例。通過在聲明屬性、變量前添加weak
關鍵字的方式使用弱引用。
當實例被銷燬時,ARC 會自動設置弱指針爲nil
。由於弱指針在運行時可能被設置爲nil
,弱指針應被聲明爲可選類型的變量,而非常量。
和其它可選類型一樣,可以檢查弱引用值是否存在,這樣就不會得到一個無效實例。
設置弱引用爲
nil
時,不會調用屬性觀察器。
使用weak
修飾之前實例Apartment
中的tenant
屬性,更新後如下:
class Person {
let name: String
init(name: String) {
self.name = name
}
var apartment: Apartment?
deinit {
print("\(name) is being deinitizlized")
}
}
class Apartment {
let unit: String
init(unit: String) {
self.unit = unit
}
// 使用weak修飾
weak var tenant: Person?
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
下圖是實例間引用關係:
Person
實例強引用Apartment
實例,但Apartment
實例沒有強引用Person
實例。當移除john
實例對Person
的強引用,Person
實例就沒有被強引用了,也就可以被銷燬了。
3.2 unowned
與weak
一樣,unowned
指針也不會對指向的對象產生強引用,但unowned
用在另一個實例生命週期一樣或更長的情況。通過在聲明屬性、變量前添加unowned
關鍵字的方式使用unowned
。
與weak
不同,unowned
修飾的引用永遠不爲空。因此,標記爲unowned
的值不是可選類型,ARC 也不會將unowned
引用設置爲nil
。
只有確信引用不會被釋放的時候才使用
unowned
,使用unowned
修飾的對象被銷燬後再次訪問會產生運行時錯誤。
現在定義兩個類:Customer
和CreditCard
。Customer
是銀行的客戶,CreditCard
是該客戶的銀行卡。Customer
和CreditCard
類都有一個屬性持有彼此,這種持有關係會產生強引用。
Customer
和CreditCard
的關係與Person
和Apartment
的關係稍有不同。Customer
可能持有CreditCard
,也可能不持有CreditCard
;但CreditCard
不會脫離Customer
而存在。即Customer
有一個可選類型的card
屬性,CreditCard
有一個 unowned 的customer
屬性。
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
,如下所示:
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: John!)
其引用關係如下:
Customer
實例強引用CreditCard
實例,CreditCard
實例 unowned Customer
實例。
當john
變量取消對實例的強引用後,就沒有強引用指向該實例,該實例就會被銷燬。該實例銷燬後,沒有強引用指向CreditCard
,其也會被銷燬。
上面的示例介紹瞭如何使用 safe unowned 引用,Swift 同時提供了 unsafe unowned 引用,其可以避免 runtime 的安全檢查,提高性能。使用 unsafe 相關操作時,開發者需自行檢查其是否存在,確保安全。
使用
unowned(unsafe)
標記 unsafe unowned 引用。當實例銷燬後,再次訪問實例會直接訪問銷燬前的內存地址。
參考資料:
- What is the difference in Swift between 'unowned(safe)' and 'unowned(unsafe)'?
- Automatic Reference Counting