自動引用計數 (Automatic Reference Counting)
自從蘋果2014年發布Swift,到現在已經兩年多了,而Swift也來到了3.1版本。去年利用工作之余,共花了兩個多月的時間把官方的Swift編程指南看完。現在整理一下筆記,回顧一下以前的知識。有需要的同學可以去看官方文檔>>。
Swift使用Automatic Reference Counting(ARC)來管理應用內存。在大多數情況下,我們不必關心內存的管理。然而,在有些情況下ARC需要更多的信息來管理內存。這個章節就來討論下這些情況。
注意:引用計數只適用于類的實例。結構和枚舉是值類型,不是引用類型。
ARC如何工作 (How ARC Works)
每當我們創建一個類的實例,ARC會分配內存來存儲這個實例的相關信息。當這個實例不再需要的時候,ARC會釋放存儲這個是實例相關信息的內存。
然而,如果ARC釋放了那些還在使用的實例,那么我們就不能再訪問實例的屬性或者調用實例的方法。如果還嘗試訪問這個實例,應用將會崩潰。
為了保證一個實例還需要使用時不會被釋放,ARC跟蹤有多少個屬性、常量和變量正在引用這個實例。只要至少還有一個引用,ARC不會釋放這個實例。
當我們把這個實例賦給一個屬性、常量或者變量,這個屬性、常量或者變量就會有一個強引用引用著這個實例。之所以被稱為“強”引用,是因為這個屬性、常量或者變量牢牢地抓住的這個實例,只要還有強引用存在,這個實例就不允許被釋放。
ARC實踐 (ARC in Action)
首先有一個Person
類:
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
下面是三個Person?
類型的變量,用于對一個Person
實例進行多個引用。
var reference1: Person?
var reference2: Person?
var reference3: Person?
創建一個Person
實例并賦給reference1:
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
因為Person
實例賦給了reference1,所以現在有一個強引用引用著Person
實例,ARC不會被釋放。
如果把reference1
賦給另外兩個變量:
reference2 = reference1
reference3 = reference1
那么現在有三個強引用引用著Persjon
實例。
把reference1
和reference2
設置為nil
,其中的兩個強引用被打斷,剩下一個強引用:
reference1 = nil
reference2 = nil
把reference3
設置為nil
,最后一個強引用被打斷,沒有其他屬性在引用著Person
實例,deinit
方法執行,Person
實例被釋放:
reference3 = nil
// Prints "John Appleseed is being deinitialized"
類實例之間的強引用循環 (Strong Reference Cycles Between Class Instances)
上面的例子跟蹤了多個強引用引用著Person
實例,當Person
實例不在引用時,被釋放。
然而,有可能寫了一些代碼使得一個類實例的強引用數量不能變為0。例如,如果兩個實例之間各有一個強引用引用著對方,那么這兩個實例的強引用數量都不能變為0,這就叫強引用循環。
要解決強引用循環,我們需要把一些強引用設置為弱引用或者無主引用。在解決強引用循環之前,我們先看看強引用循環是如何造成的。
新建Person
和Apartment
類:
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
和Apartment
類的實例:
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
下圖是目前強引用情況:
我們把兩個實例聯系起來之后,人有了公寓,公寓有了租客。
john!.apartment = unit4A
unit4A!.tenant = john
下圖是目前強引用情況:
不幸的是,兩個實例之間互相有一個強引用引用著。Person
實例有一個強引用引用著Apartment
實例,并且Apartment
實例引用著Person
實例。所以,當我們把john
和unit4A
變量設置為nil
之后,兩個實例的強引用數都不為0,所以不會被ARC釋放:
john = nil
unit4A = nil
下圖是目前強引用情況:
兩個實例之間互相有一個強引用引用著。
解決兩個實例之間的強引用循環 (Resolving Strong Reference Cycles Between Class Instance)
Swift提供了兩種方式來解決兩個實例之間的強引用循環:弱引用(weak reference)和無主引用(unowned reference)。
當其他實例的生命周期比較短時(也就是說其他實例先被釋放),使用弱引用;當其他實例有同樣或者更長的生命周期時,使用無主引用。
弱引用 (Weak References)
一個引用不會牢牢抓住它引用的實例,這就叫做弱引用。在屬性或者變量聲明時,在最前面加上weak
來提示這將會創建一個弱引用。
當弱引用引用的實例被釋放之后,ARC會自動把弱引用設置為nil
。因為弱引用要求他們的值在運行的時候能被改為nil
,所以它們總是被聲明為變量可選類型,而不是常量可選類型。
注意:當ARC把弱引用設置為nil
時,屬性觀察者不會被調用。
把上面的例子更改如下,Apartment
的屬性tenant
屬性聲明為弱引用:
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 }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
創建Person
和Apartment
實例,并聯系起來:
var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
下圖是目前引用情況:
Person
實例仍然強引用著Apartment
實例,但是Apartment
實例弱引用著Apartment
實例。這意味著,當我們把john
實例設置為nil
之后,就沒有強引用對Person
實例進行引用:
john = nil
// Prints "John Appleseed is being deinitialized"
因為沒有強引用對Person
實例進行引用,所以Person
實例被釋放,tenant
屬性被設置為nil
:
Apartment
實例只被一個強引用引用著,如果把unit4A
也設置為nil
,Apartment
實例就沒有強引用引用著:
unit4A = nil
Apartment
實例就沒有強引用引用著,也會被釋放:
無主引用 (Unowned References)
當其他實例的有同樣或更長的生命周期時,使用無主引用。在屬性或者變量聲明時,在最前面加上unowned
來提示這將會創建一個無主引用。
下面是顧客和信用卡類:
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") }
}
注意: CreditCard
的number
屬性被設置為UInt64
類型,以保證number
的取值范圍足夠存儲16位信用卡號碼。
新建一個Customer
實例:
var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
目前的引用情況如下:
Customer
實例有一個強引用引用著CreditCard
實例,CreditCard
實例有一個弱引用引用著Customer
實例。
當我們把john
設置為nil
之后,就沒有強引用引用著Customer
實例:
因為沒有強引用引用著Customer
實例,所以被釋放;Customer
實例被釋放之后,CreditCard
實例也沒有被強引用引用著,也被釋放:
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
注意:上面這個例子演示的是如何使用安全的無主引用。Swift還提供了不安全的無主引用,可以在需要禁用運行時安全檢查時使用。一旦使用了不安全無主引用,我們有責任去檢查代碼的安全性。使用unowned(unsafe)
來定義一個不安全的無主引用。不安全的無主引用引用的實例被釋放之后,如果我們還繼續訪問,那我們訪問的是那個實例之前在內存的存儲位置,這是一個不安全的操作。
無主引用和隱式解包可選類型屬性 (Unowned References and Implicitly Unwrapped Optional Properties)
上面演示了兩種常見的造成強引用循環的情況。
然而,還有第三種,兩個屬性都應該有值,并且初始化完成之后,都不應該為nil
。在這種情況下,我們要把一個類的無主屬性(unowned property)和另外一個類的隱式解包可選類型屬性結合起來。一旦初始化完成,我們就可以訪問這兩個屬性,并且避免了引用循環。
新建Country
和City
類:
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
Country
在初始化器中調用了City
的初始化器,然而在Country
實例完全初始化之前,是不能把self
屬性傳給City
的初始化器的。
為了應對這種情況,我們把Country
的capital
屬性定義為一個隱式解包可選類型屬性,這意味著capitalCity
屬性有一個默認值nil
,可以在不不用解包的情況下直接訪問。
因為capitalCity
有默認值nil
,只要name
屬性有值,那么Country
實例就被認為初始化完成。所以,在Country
的初始化器中,設置好name
的值之后,就可以把self
屬性傳遞給City
的初始化器。
我們就可以用一行代碼創建Country
和City
實例,并且沒有循環引用:
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
閉包的強引用循環 (Strong Reference Cycles for Closures)
如果我們把閉包賦給類的屬性,而這個閉包又引用著這個類的實例(包括引用這個類的屬性和方法),這也會造成強引用循環。造成循環引用是因為閉包,因為閉包向class一樣,是引用類型。
下面是演示閉包強引用循環:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
創建一個HTMLElement
實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
不幸的是,HTMLElement
實例和賦給asHTML
屬性的閉包之間有強引用循環:
asHTML
引用著閉包,閉包又引用著self.name
和self.text
。
注意:即使閉包多次引用self
,但是只有一個強引用引用著HTMLElement
實例。
把paragraph
設置為nil
,HTMLElement
實例和閉包不會被釋放,因為強引用循環:
paragraph = nil
解決閉包的強引用循環 (Resolving Strong Reference Cycles for Closures)
定義一個捕獲列表來解決閉包和類實例之間的強引用循環問題,并把捕獲列表作為閉包的一部分。
定義一個捕獲列表 (Defining a Capture List)
語法如下:
lazy var someClosure: (Int, String) -> String = {
[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
如果閉包沒有明確寫出參數或者返回值類型,因為他們可以從上下文推斷出來,那么可以簡寫成:
lazy var someClosure: () -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
弱引用和無主引用 (Weak and Unowned References)
當閉包和閉包捕獲的實例總是互相引用并同時被釋放時,把捕獲定義為無主引用。
當捕獲的引用在未來某些時候可能變為nil
時,把捕獲定義為弱引用。弱引用永遠都是一個可選類型,并且當他們引用的實例被釋放時會自動變為nil
。
注意:如果捕獲的引用從不變成nil
,一定要把捕獲定義為無主引用,而不是弱引用。
無主引用可以用于解決上面HTMLElement
這個例子的循環引用問題:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
[unowned self]
是捕獲列表,意思是把捕獲的self
作為無主引用。
創建一個HTMLElement
實例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
閉包和實例之間的引用如下:
這時,閉包對self
的引用是無主引用。如果把paragraph
設置為nil
,HTMLElement
實例將會被釋放。
paragraph = nil
// Prints "p is being deinitialized"
第十六部分完。下個部分:【Swift 3.1】17 - 可選鏈 (Optional Chaining)
如果有錯誤的地方,歡迎指正!謝謝!