Swift学习笔记(八)--析构器与ARC

析构(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的东西也差不多了, 如果对引用计数和一些细节不太清楚的, 可以看官方文档, 里面有不少图表表示的很清晰.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容