【Swift 3.1】16 - 自动引用计数 (Automatic Reference Counting)

自动引用计数 (Automatic Reference Counting)

自从苹果2014年发布Swift,到现在已经两年多了,而Swift也来到了3.1版本。去年利用工作之余,共花了两个多月的时间把官方的Swift编程指南看完。现在整理一下笔记,回顾一下以前的知识。有需要的同学可以去看官方文档>>


Swift使用Automatic Reference Counting(ARC)来管理应用内存。在大多数情况下,我们不必关心内存的管理。然而,在有些情况下ARC需要更多的信息来管理内存。这个章节就来讨论下这些情况。

注意:引用计数只适用于类的实例。结构和枚举是值类型,不是引用类型。

ARC如何工作 (How ARC Works)

每当我们创建一个类的实例,ARC会分配内存来存储这个实例的相关信息。当这个实例不再需要的时候,ARC会释放存储这个是实例相关信息的内存。

然而,如果ARC释放了那些还在使用的实例,那么我们就不能再访问实例的属性或者调用实例的方法。如果还尝试访问这个实例,应用将会崩溃。

为了保证一个实例还需要使用时不会被释放,ARC跟踪有多少个属性、常量和变量正在引用这个实例。只要至少还有一个引用,ARC不会释放这个实例。

当我们把这个实例赋给一个属性、常量或者变量,这个属性、常量或者变量就会有一个强引用引用着这个实例。之所以被称为“强”引用,是因为这个属性、常量或者变量牢牢地抓住的这个实例,只要还有强引用存在,这个实例就不允许被释放。

ARC实践 (ARC in Action)

首先有一个Person类:

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

下面是三个Person?类型的变量,用于对一个Person实例进行多个引用。

var reference1: Person?
var reference2: Person?
var reference3: Person?

创建一个Person实例并赋给reference1:

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"

因为Person实例赋给了reference1,所以现在有一个强引用引用着Person实例,ARC不会被释放。

如果把reference1赋给另外两个变量:

reference2 = reference1
reference3 = reference1

那么现在有三个强引用引用着Persjon实例。

reference1reference2设置为nil,其中的两个强引用被打断,剩下一个强引用:

reference1 = nil
reference2 = nil

reference3设置为nil,最后一个强引用被打断,没有其他属性在引用着Person实例,deinit方法执行,Person实例被释放:

reference3 = nil
// Prints "John Appleseed is being deinitialized"

类实例之间的强引用循环 (Strong Reference Cycles Between Class Instances)

上面的例子跟踪了多个强引用引用着Person实例,当Person实例不在引用时,被释放。

然而,有可能写了一些代码使得一个类实例的强引用数量不能变为0。例如,如果两个实例之间各有一个强引用引用着对方,那么这两个实例的强引用数量都不能变为0,这就叫强引用循环

要解决强引用循环,我们需要把一些强引用设置为弱引用或者无主引用。在解决强引用循环之前,我们先看看强引用循环是如何造成的。

新建PersonApartment类:

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

创建PersonApartment类的实例:

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

下图是目前强引用情况:

强引用

我们把两个实例联系起来之后,人有了公寓,公寓有了租客。

john!.apartment = unit4A
unit4A!.tenant = john

下图是目前强引用情况:

强引用

不幸的是,两个实例之间互相有一个强引用引用着。Person实例有一个强引用引用着Apartment实例,并且Apartment实例引用着Person实例。所以,当我们把johnunit4A变量设置为nil之后,两个实例的强引用数都不为0,所以不会被ARC释放:

john = nil
unit4A = nil

下图是目前强引用情况:

强引用

两个实例之间互相有一个强引用引用着。

解决两个实例之间的强引用循环 (Resolving Strong Reference Cycles Between Class Instance)

Swift提供了两种方式来解决两个实例之间的强引用循环:弱引用(weak reference)和无主引用(unowned reference)。

当其他实例的生命周期比较短时(也就是说其他实例先被释放),使用弱引用;当其他实例有同样或者更长的生命周期时,使用无主引用。

弱引用 (Weak References)

一个引用不会牢牢抓住它引用的实例,这就叫做弱引用。在属性或者变量声明时,在最前面加上weak来提示这将会创建一个弱引用。

当弱引用引用的实例被释放之后,ARC会自动把弱引用设置为nil。因为弱引用要求他们的值在运行的时候能被改为nil,所以它们总是被声明为变量可选类型,而不是常量可选类型。

注意:当ARC把弱引用设置为nil时,属性观察者不会被调用。

把上面的例子更改如下,Apartment的属性tenant属性声明为弱引用:

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

创建PersonApartment实例,并联系起来:

var john: Person? = Person(name: "John Appleseed")
var unit4A: Apartment? = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

下图是目前引用情况:

引用

Person实例仍然强引用着Apartment实例,但是Apartment实例弱引用着Apartment实例。这意味着,当我们把john实例设置为nil之后,就没有强引用对Person实例进行引用:

john = nil
// Prints "John Appleseed is being deinitialized"

因为没有强引用对Person实例进行引用,所以Person实例被释放,tenant属性被设置为nil

引用

Apartment实例只被一个强引用引用着,如果把unit4A也设置为nilApartment实例就没有强引用引用着:

unit4A = nil

Apartment实例就没有强引用引用着,也会被释放:

引用
无主引用 (Unowned References)

当其他实例的有同样或更长的生命周期时,使用无主引用。在属性或者变量声明时,在最前面加上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") }
}

注意: CreditCardnumber属性被设置为UInt64类型,以保证number的取值范围足够存储16位信用卡号码。

新建一个Customer实例:

var john: Customer? = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

目前的引用情况如下:

引用

Customer实例有一个强引用引用着CreditCard实例,CreditCard实例有一个弱引用引用着Customer实例。

当我们把john设置为nil之后,就没有强引用引用着Customer实例:

引用

因为没有强引用引用着Customer实例,所以被释放;Customer实例被释放之后,CreditCard实例也没有被强引用引用着,也被释放:

john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"

注意:上面这个例子演示的是如何使用安全的无主引用。Swift还提供了不安全的无主引用,可以在需要禁用运行时安全检查时使用。一旦使用了不安全无主引用,我们有责任去检查代码的安全性。使用unowned(unsafe)来定义一个不安全的无主引用。不安全的无主引用引用的实例被释放之后,如果我们还继续访问,那我们访问的是那个实例之前在内存的存储位置,这是一个不安全的操作。

无主引用和隐式解包可选类型属性 (Unowned References and Implicitly Unwrapped Optional Properties)

上面演示了两种常见的造成强引用循环的情况。

然而,还有第三种,两个属性都应该有值,并且初始化完成之后,都不应该为nil。在这种情况下,我们要把一个类的无主属性(unowned property)和另外一个类的隐式解包可选类型属性结合起来。一旦初始化完成,我们就可以访问这两个属性,并且避免了引用循环。

新建CountryCity类:

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

Country在初始化器中调用了City的初始化器,然而在Country实例完全初始化之前,是不能把self属性传给City的初始化器的。

为了应对这种情况,我们把Countrycapital属性定义为一个隐式解包可选类型属性,这意味着capitalCity属性有一个默认值nil,可以在不不用解包的情况下直接访问。

因为capitalCity有默认值nil,只要name属性有值,那么Country实例就被认为初始化完成。所以,在Country的初始化器中,设置好name的值之后,就可以把self属性传递给City的初始化器。

我们就可以用一行代码创建CountryCity实例,并且没有循环引用:

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"

闭包的强引用循环 (Strong Reference Cycles for Closures)

如果我们把闭包赋给类的属性,而这个闭包又引用着这个类的实例(包括引用这个类的属性和方法),这也会造成强引用循环。造成循环引用是因为闭包,因为闭包向class一样,是引用类型。

下面是演示闭包强引用循环:

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

创建一个HTMLElement实例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

不幸的是,HTMLElement实例和赋给asHTML属性的闭包之间有强引用循环:

强引用

asHTML引用着闭包,闭包又引用着self.nameself.text

注意:即使闭包多次引用self,但是只有一个强引用引用着HTMLElement实例。

paragraph设置为nilHTMLElement实例和闭包不会被释放,因为强引用循环:

paragraph = nil

解决闭包的强引用循环 (Resolving Strong Reference Cycles for Closures)

定义一个捕获列表来解决闭包和类实例之间的强引用循环问题,并把捕获列表作为闭包的一部分。

定义一个捕获列表 (Defining a Capture List)

语法如下:

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

如果闭包没有明确写出参数或者返回值类型,因为他们可以从上下文推断出来,那么可以简写成:

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}
弱引用和无主引用 (Weak and Unowned References)

当闭包和闭包捕获的实例总是互相引用并同时被释放时,把捕获定义为无主引用。

当捕获的引用在未来某些时候可能变为nil时,把捕获定义为弱引用。弱引用永远都是一个可选类型,并且当他们引用的实例被释放时会自动变为nil

注意:如果捕获的引用从不变成nil,一定要把捕获定义为无主引用,而不是弱引用。

无主引用可以用于解决上面HTMLElement这个例子的循环引用问题:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> 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")
    }
    
}

[unowned self]是捕获列表,意思是把捕获的self作为无主引用。

创建一个HTMLElement实例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

闭包和实例之间的引用如下:

引用

这时,闭包对self的引用是无主引用。如果把paragraph设置为nilHTMLElement实例将会被释放。

paragraph = nil
// Prints "p is being deinitialized"

第十六部分完。下个部分:【Swift 3.1】17 - 可选链 (Optional Chaining)


如果有错误的地方,欢迎指正!谢谢!

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

推荐阅读更多精彩内容