析構(gòu)(Deinitialization)
析構(gòu)這一塊因為ARC的原因, 所以并沒有太多的代碼要寫, 因此也比較簡單, 基本上可以和ObjC里面的dealloc對應起來, 都是做一些收尾的工作. 需要注意的是, 只有類才有析構(gòu)器.
析構(gòu)過程是怎樣的(How Deinitialization Works)
Swift也是ARC的內(nèi)存管理方式, 因此, 需要我們在析構(gòu)中做的, 基本上就是釋放一些實例持有的資源, 例如在實例被析構(gòu)前關閉掉打開的文件, 移除NotificationCenter的監(jiān)聽等等.
析構(gòu)器通過deinit來定義, 和init不同的是, 它不需要參數(shù), 所以連括號都被省略掉了:
deinit {
// perform the deinitialization
}
析構(gòu)器會在實例被釋放的時候自動調(diào)用, 不需要也禁止我們手動去執(zhí)行. 父類的析構(gòu)器會被子類繼承下來, 也會在子類的析構(gòu)器執(zhí)行完畢后被執(zhí)行. 在析構(gòu)器中可以訪問全部的屬性, 所以也可以進一步操作實例(例如釋放文件)
析構(gòu)示例
文檔直接給了一個例子來說明析構(gòu)的整個過程, 我們直接看看吧:
class Bank {
static var coinsInBank = 10_000
static func vendCoins(var numberOfCoinsToVend: Int) -> Int {
numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receiveCoins(coins: Int) {
coinsInBank += coins
}
}
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.vendCoins(coins)
}
func winCoins(coins: Int) {
coinsInPurse += Bank.vendCoins(coins)
}
deinit {
Bank.receiveCoins(coinsInPurse)
}
}
// 創(chuàng)建對象
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// prints "A new player has joined the game with 100 coins"
print("There are now \(Bank.coinsInBank) coins left in the bank")
// prints "There are now 9900 coins left in the bank"
// 操作對象
playerOne!.winCoins(2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// prints "PlayerOne won 2000 coins & now has 2100 coins"
print("The bank now only has \(Bank.coinsInBank) coins left")
// prints "The bank now only has 7900 coins left"
// 析構(gòu)對象
playerOne = nil
print("PlayerOne has left the game")
// prints "PlayerOne has left the game"
print("The bank now has \(Bank.coinsInBank) coins")
// prints "The bank now has 10000 coins"
析構(gòu)對象后, 執(zhí)行了deinit里面的代碼, 所以硬幣又回到了Bank
至此, 官網(wǎng)的文檔就結(jié)束了, 基本上也沒太多可講的, 文檔通篇甚至連個NOTE都沒有. 具體細節(jié)看官方文檔
自動引用計數(shù)器(Automatic Reference Counting)
自動引用計數(shù)器在ObjC里面就已經(jīng)被應用了, 所以不打算講一些細節(jié)了, 直接看一些有差別的部分好了.
和ObjC一樣, Swift里面兩個對象互相強引用就會造成循環(huán)引用, 導致內(nèi)存泄露. 在ObjC中, 我們用__weak表示弱引用, 從而打破循環(huán)引用鏈, 在Swift里面有兩種解決方法, 一是傳統(tǒng)的弱引用, 另一種是非持有引用. 這兩者都可以以不持有的方式引用一個對象, 和ObjC一樣, weak會自動把持有的對象置空(因此需要weak的對象不能聲明為常量), 所以兩者具體使用場景有一定的區(qū)別, 語法上當然也會有所區(qū)別. 如果需要持有的對象可能為空, 那么用weak, 如果需要持有的對象一直都有值, 那么用unowned(這里的一直和可能是針對于對象本身的生命周期而言的, 其實也可以簡單認為, 兩個對象, 如果生命周期不一致, 則用weak, 如果一個對象A明顯在另一個對象B存在的時候一定存在, 則B要用unowned來引用A). 下面分別詳細闡述.
弱引用
在Swift里面, 用weak來修飾需要弱引用的對象. 如之前所述, 可能為空則用weak, 例如, 對于公寓(Apartment)這個對象來說, 可能是沒有租客(tenant)的, 所以, 用weak更合適.
以官網(wǎng)的例子來看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 } // 因為tenant是Optional的, 所以不需要手動init
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
// 互相持有了
john!.apartment = unit4A
unit4A!.tenant = john
// 釋放tenant對象
john = nil
// prints "John Appleseed is being deinitialized
unit4A.tenant // 輸出nil
// 釋放apartment對象
unit4A = nil
// prints "Apartment 4A is being deinitialized"
從上面的例子可以看到, 雖然互相持有了, 但是weak打破了這個循環(huán), 因此并沒有內(nèi)存泄露.
需要注意的是, 在垃圾回收的系統(tǒng)中(Swift和ObjC都有), 弱指針有時會被用作一個簡單的緩存機制, 因為非強引用的對象只有當內(nèi)存壓力觸發(fā)垃圾回收才會析構(gòu)掉. 但是, 在ARC中, 當引用計數(shù)器歸零的時候, 對象會被盡快地移除, 所以弱引用并不適合用于盡快移除的目的.
非持有引用
與weak一樣, 都不會強引用一個對象, 區(qū)別也如之前所提, 是假設被引用的對象是一直都有值的. 因此, 一個unowned引用不能被定義為optional(這也就是之前提的語法會有些許區(qū)別的原因), 同時如果引用的對象被釋放后, 引用指針也不會自動置空. 如果需要非持有引用一個對象, 用unowned來修飾.
需要注意的是, 如果在unowned引用對象被釋放后, 還去訪問這個對象的話, 會導致runtime錯誤. 所以, 只有確保引用的對象一直有值的情況下, 才使用unowned, 否則你的app會crash.
下面來看看文檔給出的例子:
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") }
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
// 釋放對象
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized" Card被自動釋放掉了
非持有引用和隱式拆包可選屬性(Unowned References and Implicitly Unwrapped Optional Properties)
上面的兩個例子分別解決了, 兩個對象都可能為nil的情況, 和一個為nil一個不為nil的情況, 但是還有一種情況是, 兩個都不為nil的情況. 在這種情況下, 就需要用到一個類用unowned屬性, 另一個類有隱式拆包的可選屬性的混合體了. 這種做法可以在初始化完成后直接訪問兩邊的屬性(不需要拆包), 而且也不會造成循環(huán)引用.
(其實就是把問號(?)改成了感嘆號(!), 如我之前所說的, ?比!更加安全, 當然, 蘋果這里這么寫是因為前提是你非常確定兩者都不為nil的情況.)
直接看官方給出的例子:
class Country {
let name: String
var capitalCity: City! // <-- 就這么點區(qū)別
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
}
}
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"
看起來很高大上, 但其實還是以前講過的老內(nèi)容, 還是再次提醒, 這種用法只用于很確定兩者都不為nil的情況
閉包的強引用循環(huán)
應該在第一章說過, 閉包相當于ObjC里面的block, 在block里面, 我們也要面對可能的循環(huán)引用, 同樣的, 在閉包中, 我們也要小心處理強引用的情況.
因為兩者相似度過高, 基本會遇到的情況都相似, 所以直接看官方的例子:
class HTMLElement {
let name: String
let text: String?
// 顯然 HTMLElement實例持有asHTML這個閉包,
// 然后閉包(也是對象)在內(nèi)部訪問了HTMLElement實例的屬性, 也就對它有了強引用, 所以就造成了循環(huán)引用
// 也許是為了讓我們避免不用self就不會有強引用的假象, 蘋果直接不讓你省略self了
// 另外, 上一章提了, 不能在默認值這里使用類本身的屬性, 但是這里用了, 是因為有l(wèi)azy
lazy var asHTML: Void -> 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")
}
}
// 創(chuàng)建了HTMLElement, 但是因為lazy的關系, 還沒有循環(huán)引用
var html:HTMLElement? = HTMLElement(name: "Ryan")
weak var tmp = html
html = nil
tmp // playground輸出nil
// 創(chuàng)建了HTMLElement, 又訪問了asHTML, 因此創(chuàng)建了閉包, 所以造成循環(huán)引用
var html2:HTMLElement? = HTMLElement(name: "Ryan")
html2?.asHTML
weak var tmp2 = html2
html2 = nil
tmp2 // playground輸出HTMLElement, 證明實例未析構(gòu)
順便提一句, 閉包會強引用實例的原因是因為capture list這個東西, 這個東西可以簡單理解為參數(shù)表(但是不是閉包本身的參數(shù), 具體可以看看block的實現(xiàn), 原理應該差不多, 或者直接看Swift的源碼, 都全部開源了還猶豫什么!!!), 在默認情況下會對引用類型做強引用, 因此, 為了打破這個強引用, 也就需要對它出手了.
直接看寫法了吧, 其實也就是自己定義一下Capture List(或者說在capture list里面聲明一下, 哪些是要weak的, 哪些是要unowned的):
lazy var someClosure: Void -> String = {
[unowned self, weak delegate = self.delegate!] in
// closure body goes here
}
復習一下上一節(jié), 因為self肯定在這個閉包的生命周期中是一直有的, 所以是unowned, delegate則不一定, 所以是weak.
所以, 上面的那個例子要改寫的話就是這樣的:
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: Void -> 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")
}
}
var html2:HTMLElement? = HTMLElement(name: "Ryan")
html2?.asHTML
weak var tmp2 = html2
html2 = nil
tmp2 // playground打印出nil
至此, 就解決了閉包出現(xiàn)循環(huán)引用的情況了. 到這里ARC的東西也差不多了, 如果對引用計數(shù)和一些細節(jié)不太清楚的, 可以看官方文檔, 里面有不少圖表表示的很清晰.