ARC:"Automatic Reference Counting",自動引用計數。Swift語言延續了OC的做法,也是利用ARC機制進行記憶體管理,和OC的ARC一樣,當一些類的例項不在需要的時候,ARC會釋放它們的記憶體。但是,在少數情況下,ARC需要知道你的程式碼之間的關係才能更好的為你管理記憶體,和OC一樣,Swift中的ARC也存在迴圈引用導致記憶體洩露的情況。
一、ARC的工作機制
每當我們建立一個類的新的例項的時候,ARC會從堆中分配一塊記憶體用來儲存有關該例項的資訊。這塊記憶體將持有這個例項的型別資訊以及和它關聯的屬性的值。另外,當這個例項不再被需要的時候,ARC將回收這個例項所佔有的記憶體並且將這部分記憶體給其他需要的例項用。這樣就能保證不再被需要的例項不佔用多餘的記憶體。 但是,如果ARC釋放了正在使用的例項,那麼該例項的屬性將不能被訪問,方法將不能被呼叫,如果你訪問它的屬性或者呼叫它的方法時,應用會崩潰,因為你訪問了一個野指標。 為了解決上述問題,ARC會跟蹤每個類的例項正在被多少個屬性、常量或者變數引用,每當你將類例項賦值給屬性,常量或者變數的時候它就會被"強"引用一次,當它的引用計數為0時,表明它不再被需要,ARC就會銷燬它。 下面舉個例子介紹ARC是如何工作的
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
複製程式碼
上述程式碼建立了一個名為Person
的類,該類宣告瞭一個非可選的型別的name
常量,一個給name
賦值的初始化方法,並且列印了一句話,用來標註初始化成功,同時宣告瞭一個解構函式,列印了一句標誌此例項被銷燬的資訊。
var reference1: Person?
var reference2: Person?
var reference3: Person?
複製程式碼
上述程式碼宣告瞭三個Person?
型別的變數,這三個變數為可選型別,所以被自動初始化為nil
,此時三個例項都沒有指向任何一個Person
類的例項。
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
複製程式碼
現在建立一個Person
類的例項,並且賦值給reference1
,此時控制檯會列印"John Appleseed is being initialized"
。
reference2 = reference1
reference3 = reference1
複製程式碼
然後將該例項賦值給reference2
和reference3
。現在該例項被三個"強"型別的指標引用。
reference1 = nil
reference2 = nil
複製程式碼
如上所示,當我們將其中兩個引用賦值給nil
的時候,這兩個"強"引用被打破,但是這個Person
的例項並沒有被釋放(釋放資訊未列印),因為還存在一個對這個例項的強引用。
reference3 = nil
// Prints "John Appleseed is being deinitialized"
複製程式碼
當我們將第三個"強"引用打破的時候(賦值為nil
),可以看到控制檯列印的"John Appleseed is being deinitialized"
析構資訊。
二、兩個類例項之間的迴圈引用
上述的例子中,ARC可以很好的獲取一個例項的引用計數,並且當它的引用計數為0的時候釋放它。但是在實際的開發過程中,會存在一些特殊情況,使ARC沒辦法得到引用計數為0這個關鍵點,就會造成這個例項的記憶體一直不被釋放,兩個類的例項相互"強"引用就會造成這種情況,就是"迴圈引用"。
蘋果官方提供了兩種方法來解決兩個例項之間的迴圈引用,unowned
引用和weak
引用。
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
類。每一個Person
的例項都有一個name
的屬性和一個apartment
的可選屬性,初始化為nil
,因為並不是每一個人都擁有一個公寓,所以是可選屬性。同樣的,每一個Apartment
例項都有一個unit
屬性和一個tenant
的可選屬性,初始化為nil
,同理,不是每一個公寓都有人租。同時,兩個類都定義了deinit
方法,並且列印一段資訊,用來讓我們清楚這個例項何時被銷燬。
var john: Person?
var unit4A: Apartment?
複製程式碼
分別定義一個Person
型別和Apartment
的變數,定義為optional
(可選型別),初始化為nil
。
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
複製程式碼
然後分別建立一個Person
類的例項和Apartment
類的例項,並且分別賦值給上面的定義的變數。
john
將擁有一座公寓unit4A
,公寓unit4A
將被john
承租。
john!.apartment = unit4A
unit4A!.tenant = john
複製程式碼
因為可以確定兩個變數都被賦值為相應型別的例項,所以此處用!
對可選屬性強解包。
此時,兩個變數和例項以及兩個例項之間的"強"引用關係如下圖。
john = nil
unit4A = nil
複製程式碼
當我們將兩個變數設定為nil
,切斷他們與例項之間的"強"引用關係,此時兩個例項之間的"強"引用關係為:
三、解決兩個類例項之間的迴圈引用
Swift提供了兩種辦法解決類例項之間的迴圈引用。weak
引用和unowned
引用。這兩種方法都可以使一個例項引用另一個例項的時候,不用保持"強"引用。weak
一般應用於其中一個例項具有更短的生命週期,或者可以隨時設定為nil
的情況下;unowned
用於兩個例項具有差不多長的生命週期,或者說兩個例項都不能被設定為nil
。
(1) weak引用
weak
引用對所引用的例項不會保持"強"引用的關係。假如一個例項同時被若干個"強引用"和一個weak
引用引用時,當所有其他的"強"引用都被打破時該例項就會被ARC釋放,並且ARC會自動將這個weak
引用置為nil
。因此,weak
引用一般被宣告為var
,因為它會被ARC設定為nil
。
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") }
}
複製程式碼
現在,我們將Apartment
類中的tenant
變數宣告為weak
引用(在var
關鍵字前加weak
關鍵字),表明某公寓的承租人並不一定一直都是同一個人。
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
複製程式碼
然後和上文一樣,將兩個變數和例項關聯。此時,它們之間的引用關係如下圖。
Person
例項仍然"強"引用Apartment
例項,但是Apartment
例項'weak'引用Person
例項。john
和unit4A
兩個變數仍然"強"引用兩個例項。當我們把john
變數對Person
例項的"強"引用打破的時候,即將john
設定為nil
,就沒有其他的"強"引用引用Person
例項,此時,Person
例項被ARC釋放,同時Apartment
例項的tenant
變數被設定為nil
。
john = nil
// Prints "John Appleseed is being deinitialized"
複製程式碼
然後將變數unit4A
設為nil
,可以看到Apartment
例項也被銷燬。
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
複製程式碼
(2) unowned引用
和weak
引用一樣,unowned
引用也不會保持它和它所引用例項之間的"強"引用關係,而是保持一種非擁有(或未知)的關係,使用的時候也是用unowned
關鍵字修飾宣告的變數。不同的是,兩個互相引用的物件具有差不多長的生命週期,而不是其中一個可以提前被釋放(weak
),有點患難與共的意思。
Swift要求unowned
修飾的變數必須一直指向一個例項,而不是有些時候為nil
,因此,ARC也不會將這個變數設定為nil
,所以我們一般將這個引用宣告為非可選型別。PS:請確保你宣告的變數一直指向一個例項,如果這個例項被釋放了,而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") }
}
複製程式碼
上面這個例子定義了兩個類:Customer
和CreditCard
,每個顧客都可能會有一張信用卡(可選型別),每個信用卡都一定會有一個持有他們的顧客(非可選型別,卡片為顧客定製)。因此,Customer
類有一個CreditCard?
型別的屬性,CreditCard
類也有一個Customer
型別的屬性,並且被宣告為unowned
,以此來打破迴圈引用。每張信用卡初始化的時候都需要一名持有它的顧客,因為信用卡本身就是為顧客定製的。
var john: Customer?
複製程式碼
然後宣告一個Customer?
型別的變數john
,初始化為nil
。接著建立一個Customer
的例項,並且將它賦值給john
(讓john
引用它、指向它都是一個意思)。
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
複製程式碼
(第一句程式碼賦值之後,我們知道john
肯定不是nil
,所以用!
解包不會有問題)
然後,兩個例項之間的引用關係為:
Customer
例項"強"引用CreditCard
例項,CreditCard
例項'unowned'引用Customer
例項,接著,我們將john
對Customer
例項的"強"引用打破,即將john
設定為nil
。
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
複製程式碼
可以看到Customer
例項和CreditCard
例項都被銷燬了。john
被設定為nil
之後,就沒有"強"引用引用Customer
例項,所以,Customer
例項被釋放,也就沒有"強"引用引用CreditCard
例項,因此CreditCard
例項也被釋放。
以上例子證明,兩種方式都可以解決迴圈引用的問題,但是要注意它們使用的範圍。weak
修飾的變數可以被設定為nil
(引用的例項的生命週期短於另一個例項),unowned
修飾的變數必須要指向一個例項(造成迴圈引用的兩例項的生命週期差不多長,不會出現一方被提前釋放的情況),一旦它被釋放了,就千萬別再使用了。
四、閉包引起的迴圈引用
Swift中的閉包是一種獨立的函式程式碼塊,它可以像一個類的例項一樣在程式碼中賦值、呼叫和傳遞,也可以被認為某個匿名函式的例項,其實就是OC中的block。它和類一樣也是引用型別的,所以它的函式體中使用的引用都是"強"引用。
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")
}
}
複製程式碼
上述例子中,閉包被賦值給asHTML
變數,所以閉包被HTMLElement
例項"強"引用,而閉包又捕獲(關於閉包捕獲變數,參考官方文件Capturing Values)了HTMLElement
的例項中的text
和name
屬性,因此它又"強"引用HTMLElement
例項,這樣就造成了迴圈引用,因為text
屬性可能為空,所以定義為可選屬性。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
複製程式碼
我們建立一個HTMLElement
例項,並將它賦值給paragraph
變數,然後訪問它的asHTML
屬性。此時的記憶體示例為下圖,可以看到HTMLElement
例項和閉包之間的迴圈引用。
paragraph
設定為nil
時,控制檯並沒有列印任何銷燬資訊,因為迴圈引用。
上圖為使用Instruments分析得到的迴圈引用以及造成的記憶體洩漏。
五、使用unowned和weak解決迴圈引用
通過上文(三)的分析,我們知道unowned
引用對例項的非擁有關係,因此,我們可以通過如下方式解決迴圈引用:
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
複製程式碼
[unowned self] in
,這段程式碼,代表閉包中的self
指標都被unowned
修飾。這樣就可以使閉包對例項的"強"引用變成'unowned'引用,從而打破迴圈引用。
當HTML的element為標題的時候,此時如果text
屬性為空,我們想返回一個預設的text作為標題,而不是隻有<h/>
這種標籤。
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
複製程式碼
這段程式碼也會造成HTMLElement
對其自身的迴圈引用。我們仍然可以使用unowned
關鍵字打破迴圈引用:
heading.asHTML = {
[unowned heading] in
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
// Prints "<h1>some default text</h1>"
// Prints "h1 is being deinitialized"
複製程式碼
unowned
會使閉包中對heading
的"強"都改為'unowned'引用。
或者,可以使用weak
屬性打破迴圈引用:
weak var weakHeading = heading
heading.asHTML = {
return "<\(weakHeading!.name)>\(weakHeading!.text ?? defaultText)</\(weakHeading!.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"
複製程式碼
上文(三)中可知,weak
修飾的變數為可選型別,而且,我們對變數進行了一次賦值,就可以確保weakHeading
指向heading
引用的例項,所以可以放心的使用!
對它解包。
上面這段程式碼同樣可以使閉包對HTMLElement
例項的"強"引用變為weak
引用,從而打破迴圈引用。
(ARC會自動回收不被使用的物件,所以不用手動將變數設定為nil
)