Swift學習筆記(八)--析構器與ARC

weixin_33806914發表於2016-01-27

析構(Deinitialization)

析構這一塊因為ARC的原因, 所以並沒有太多的程式碼要寫, 因此也比較簡單, 基本上可以和ObjC裡面的dealloc對應起來, 都是做一些收尾的工作. 需要注意的是, 只有類才有析構器.

析構過程是怎樣的(How Deinitialization Works)

Swift也是ARC的記憶體管理方式, 因此, 需要我們在析構中做的, 基本上就是釋放一些例項持有的資源, 例如在例項被析構前關閉掉開啟的檔案, 移除NotificationCenter的監聽等等.
析構器通過deinit來定義, 和init不同的是, 它不需要引數, 所以連括號都被省略掉了:

deinit {
    // perform the deinitialization
}

析構器會在例項被釋放的時候自動呼叫, 不需要也禁止我們手動去執行. 父類的析構器會被子類繼承下來, 也會在子類的析構器執行完畢後被執行. 在析構器中可以訪問全部的屬性, 所以也可以進一步操作例項(例如釋放檔案)

析構示例

文件直接給了一個例子來說明析構的整個過程, 我們直接看看吧:

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)
    }
}
// 建立物件
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"

// 析構物件
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"

析構物件後, 執行了deinit裡面的程式碼, 所以硬幣又回到了Bank

至此, 官網的文件就結束了, 基本上也沒太多可講的, 文件通篇甚至連個NOTE都沒有. 具體細節看官方文件

自動引用計數器(Automatic Reference Counting)

自動引用計數器在ObjC裡面就已經被應用了, 所以不打算講一些細節了, 直接看一些有差別的部分好了.

和ObjC一樣, Swift裡面兩個物件互相強引用就會造成迴圈引用, 導致記憶體洩露. 在ObjC中, 我們用__weak表示弱引用, 從而打破迴圈引用鏈, 在Swift裡面有兩種解決方法, 一是傳統的弱引用, 另一種是非持有引用. 這兩者都可以以不持有的方式引用一個物件, 和ObjC一樣, weak會自動把持有的物件置空(因此需要weak的物件不能宣告為常量), 所以兩者具體使用場景有一定的區別, 語法上當然也會有所區別. 如果需要持有的物件可能為空, 那麼用weak, 如果需要持有的物件一直都有值, 那麼用unowned(這裡的一直和可能是針對於物件本身的生命週期而言的, 其實也可以簡單認為, 兩個物件, 如果生命週期不一致, 則用weak, 如果一個物件A明顯在另一個物件B存在的時候一定存在, 則B要用unowned來引用A). 下面分別詳細闡述.

弱引用

在Swift裡面, 用weak來修飾需要弱引用的物件. 如之前所述, 可能為空則用weak, 例如, 對於公寓(Apartment)這個物件來說, 可能是沒有租客(tenant)的, 所以, 用weak更合適.
以官網的例子來看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打破了這個迴圈, 因此並沒有記憶體洩露.
需要注意的是, 在垃圾回收的系統中(Swift和ObjC都有), 弱指標有時會被用作一個簡單的快取機制, 因為非強引用的物件只有當記憶體壓力觸發垃圾回收才會析構掉. 但是, 在ARC中, 當引用計數器歸零的時候, 物件會被儘快地移除, 所以弱引用並不適合用於儘快移除的目的.

非持有引用

與weak一樣, 都不會強引用一個物件, 區別也如之前所提, 是假設被引用的物件是一直都有值的. 因此, 一個unowned引用不能被定義為optional(這也就是之前提的語法會有些許區別的原因), 同時如果引用的物件被釋放後, 引用指標也不會自動置空. 如果需要非持有引用一個物件, 用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屬性, 另一個類有隱式拆包的可選屬性的混合體了. 這種做法可以在初始化完成後直接訪問兩邊的屬性(不需要拆包), 而且也不會造成迴圈引用.

(其實就是把問號(?)改成了感嘆號(!), 如我之前所說的, ?比!更加安全, 當然, 蘋果這裡這麼寫是因為前提是你非常確定兩者都不為nil的情況.)

直接看官方給出的例子:

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
    }
}

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"

看起來很高大上, 但其實還是以前講過的老內容, 還是再次提醒, 這種用法只用於很確定兩者都不為nil的情況

閉包的強引用迴圈

應該在第一章說過, 閉包相當於ObjC裡面的block, 在block裡面, 我們也要面對可能的迴圈引用, 同樣的, 在閉包中, 我們也要小心處理強引用的情況.
因為兩者相似度過高, 基本會遇到的情況都相似, 所以直接看官方的例子:

class HTMLElement {
    
    let name: String
    let text: String?
    
    // 顯然 HTMLElement例項持有asHTML這個閉包, 
    // 然後閉包(也是物件)在內部訪問了HTMLElement例項的屬性, 也就對它有了強引用, 所以就造成了迴圈引用
    // 也許是為了讓我們避免不用self就不會有強引用的假象, 蘋果直接不讓你省略self了
    // 另外, 上一章提了, 不能在預設值這裡使用類本身的屬性, 但是這裡用了, 是因為有lazy
    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")
    }
}
// 建立了HTMLElement, 但是因為lazy的關係, 還沒有迴圈引用
var html:HTMLElement? = HTMLElement(name: "Ryan")
weak var tmp = html
html = nil
tmp   // playground輸出nil
// 建立了HTMLElement, 又訪問了asHTML, 因此建立了閉包, 所以造成迴圈引用
var html2:HTMLElement? = HTMLElement(name: "Ryan")
html2?.asHTML
weak var tmp2 = html2
html2 = nil
tmp2  // playground輸出HTMLElement, 證明例項未析構

順便提一句, 閉包會強引用例項的原因是因為capture list這個東西, 這個東西可以簡單理解為參數列(但是不是閉包本身的引數, 具體可以看看block的實現, 原理應該差不多, 或者直接看Swift的原始碼, 都全部開源了還猶豫什麼!!!), 在預設情況下會對引用型別做強引用, 因此, 為了打破這個強引用, 也就需要對它出手了.

直接看寫法了吧, 其實也就是自己定義一下Capture List(或者說在capture list裡面宣告一下, 哪些是要weak的, 哪些是要unowned的):

lazy var someClosure: Void -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}

複習一下上一節, 因為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

至此, 就解決了閉包出現迴圈引用的情況了. 到這裡ARC的東西也差不多了, 如果對引用計數和一些細節不太清楚的, 可以看官方文件, 裡面有不少圖表表示的很清晰.

相關文章