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

析構(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é)不太清楚的, 可以看官方文檔, 里面有不少圖表表示的很清晰.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贿条,一起剝皮案震驚了整個濱河市仁热,隨后出現(xiàn)的幾起案子蚪缀,更是在濱河造成了極大的恐慌,老刑警劉巖爽醋,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齐莲,死亡現(xiàn)場離奇詭異恃慧,居然都是意外死亡肺魁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門每庆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筐带,“玉大人,你說我怎么就攤上這事缤灵÷准” “怎么了蓝晒?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長帖鸦。 經(jīng)常有香客問我芝薇,道長,這世上最難降的妖魔是什么作儿? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任洛二,我火速辦了婚禮,結(jié)果婚禮上攻锰,老公的妹妹穿的比我還像新娘晾嘶。我一直安慰自己,他們只是感情好娶吞,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布垒迂。 她就那樣靜靜地躺著,像睡著了一般妒蛇。 火紅的嫁衣襯著肌膚如雪机断。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天绣夺,我揣著相機與錄音吏奸,去河邊找鬼。 笑死陶耍,一個胖子當著我的面吹牛苦丁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播物臂,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼产上!你這毒婦竟也來了棵磷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤晋涣,失蹤者是張志新(化名)和其女友劉穎仪媒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谢鹊,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡算吩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了佃扼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片偎巢。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兼耀,靈堂內(nèi)的尸體忽然破棺而出压昼,到底是詐尸還是另有隱情求冷,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布窍霞,位于F島的核電站匠题,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏但金。R本人自食惡果不足惜韭山,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冷溃。 院中可真熱鬧钱磅,春花似錦、人聲如沸秃诵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菠净。三九已至禁舷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毅往,已是汗流浹背牵咙。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留攀唯,地道東北人洁桌。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像侯嘀,于是被迫代替她去往敵國和親另凌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容