Swift 中的内存管理 —— 看看 ARC

Swift 作为一门现代的高级语言,自然少不了自动内存管理。我们知道,JAVA 中的内存管理是通过垃圾回收完成的。作为从 Objective-C 时代演变过来的 Swift,自然也继承了很多 OC 的特性。OC 中的内存管理分为两种:一种是垃圾回收,这种内存管理方式只被运用于 OS X(现在应该叫 macOS)中;另一种就是我们经常提到的 ARC(自动引用计数),这种管理方式被同时用在 iOS 和 macOS 上。在 Swift 中,内存管理依然是通过 ARC 进行的,下面我们就深入地看一看。

什么是 ARC?

ARC,中文名叫自动引用计数,全名叫 Automatic Reference Counting。顾名思义,意思就是“自动的”引用计数。

在我们深入了解 ARC 之前,我们先看一看什么叫做引用计数。我们先通过一个类来看一看 Swift 中对象在内存中的各个阶段。

假如我们有一个 Person 类:

class Person {
    var name: String

    init(name: String) {
        self.name = name
        print("Person \(name) has been initialized.")
    }

    deinit {
        print("Person \(name) has been deallocated.")
    }
}

这个简单的 Person 类,我们给它指定了一个 name 的属性,同时提供了 init 构造方法和 deinit 析构方法。在构造方法中,我们让控制台输出一条语句,提醒我们某个对象已经初始化。同时在析构方法中,我们同样让控制台输出某个对象的内存已经释放。

首先我们创建一个 person1 对象,然后看看控制台的输出:

do {
    let person1 = Person(name: "Kenneth")
}
控制台

可以看到在 do 语法块内,我们创建的对象先进行了初始化,然后运行完毕后释放了内存。

Swift 中一个对象的生存周期也正如此,包含:

  1. 内存分配 (从栈或者堆中获得内存)
  2. 初始化 (执行 init 函数)
  3. 使用 (对象的使用)
  4. 析构(执行析构函数)
  5. 内存释放 (内存返回到栈或者堆内)

ARC 正是基于这个周期帮助我们管理内存的。当我们创建一个对象,保存一个对这个对象的引用,ARC 就一直监视我们对于该对象的引用,并记下当前有多少个对于该对象的引用。当引用数量 >0 时,内存将不会被释放。当引用数量等于 0 时,ARC 认为该对象不再被需要,于是便自动地帮助我们销毁该对象,并释放该对象的内存。

ARC

ARC 失效?

在大多数情况下,ARC 工作地很好,这也让我们不需要对内存管理花太多心思。ARC 遵循的原则简单而且有效,但它并不是万能的。在 ARC 的管理下,内存泄漏仍然可能发生。

让我们想象这样一个情景:我们有两个对象,这两个对象不再被需要,但是这两个对象互相引用了对方。

在上述情境中我们实际上不需要这两个对象,但是因为这两个对象互相引用了对方,这两个对象的引用计数都是 1。因为这两个对象的引用计数都是 1,ARC 将永远不会释放这两个对象的内存。

在这种情况下,内存泄漏就不可避免地发生。我们把这种情况叫做引用循环,更准确的说,是强引用循环。

强引用循环

让我们用一段代码来演示这个场景。

首先我们新建一个简单的 iPhone 类,这个类有两个属性,一个是型号,一个是主人:

class iPhone {
    let model: String
    var owner: Person?
    
    init(model: String) {
        self.model = model
        print("iPhone \(model) has benn initialized.")
    }
    
    deinit {
        print("iPhone \(model) has benn deallocated")
    }
}

然后我们在 do 语法块里面新建一个 iPhone 对象:

do {
    let person1 = Person(name: "Kenneth")
    let iphone1 = iPhone(model: "7 Plus")
}

然后我们看控制台的输出语句,这个时候 ARC 工作正常,两个对象都在运行完成后被释放了内存:

控制台

下面我们在 Person 类中新建一个属性,这个属性的类型是一个 iPhone 类的数组,表示该人拥有的所有 iPhone。同时我们将该属性的 setter 方法设为私有,这样强制了使用我们自定义的方法进行 set。

private(set) var iphones: [iPhone] = [iPhone]()
    func add(iphone: iPhone) {
        iphones.append(iphone)
        iphone.owner = self
    }

然后我们在 do 语法块中添加一条语句:

do {
    let person1 = Person(name: "Kenneth")
    let iphone1 = iPhone(model: "7 Plus")
    person1.add(iphone: iphone1)
}

这时候我们再看控制台:

少了两条内存释放的控制台

可以看到这个时候的控制台少输出了两天释放内存的提示。这说明我们刚才的这两个对象并没有被释放。

为什么?

就像我们上面所说的一样,这个时候发生了强引用循环。我们的 person1 对象中的 iphones 属性保有了 iphone1 对象,而我们 iphone1 中的 owner 属性又保有了 person1。这个就是我们所说的强引用循环。

破解之道?

弱引用(Weak Reference)

为了打破所谓的强引用循环,我们开始思考,有没有一种办法能够让我们同时可以进行引用但是又不增加引用计数呢?弱引用就是这样诞生的。

所谓弱引用,就是很弱的引用——它引用一个对象而不增加该对象的引用计数。

弱引用

上图中,虚线箭头表示一个弱引用。可以看到对象 2 弱引用对象 1 时,对象 1 的引用计数并没有增加。

当变量 1 和变量 2 不存在后,对象 1 的引用计数变成 0,对象 1 会被 ARC 销毁,销毁后对象 1 对对象 2 的引用也不存在,对象 2 也会被 ARC 销毁。强引用循环就这样被打破了。

针对上一段的代码,我们只要在 iPhone 类的 owner 属性前加一个 weak,即可以把该引用改为弱引用:

weak var owner: Person?

这时候我们再看控制台:

控制台

可以看到这个时候两个对象的内存都会被正常释放,弱引用起作用了。

你可能会问,那我们保存的弱引用这个时候指向了什么呢?我们回过头来看一看 owner 属性的类型,实际上是一个 Optional。聪明的你肯定能猜到,弱引用指向的对象如果内存被释放,会自动变为空引用,也就是 nil。

无主引用(Unowned Reference)

弱引用 weak 作为解决强引用循环的一个关键字,它将会在被引用对象的内存释放后变为 nil。Swift 中还提供了另外一个关键字——unowned,它也能达到引用对象而不增加其引用计数的效果。和 weak 关键字不同的是,weak 关键字会在对象内存释放后变为 nil,而 unowned 会继续保有对该对象的引用,即使该引用已经变为无效。

在上一段,我们知道 weak 关键字描述的属性必须申明为可变变量并且是 Optional 类型,这是因为它可能会变成 nil。

unowned 关键字则必须不是 Optional 类型,也就是说我们在访问 unowned 变量时,不需要进行对于 Optional 类型的解包操作,可以直接访问。然而这也造成了如果我们的 unowned 变量所指向的对象内存被释放后,我们再访问这个变量会出现运行时错误。

三种引用可使用的情景:

| | var | let | Optional | 非 Optional |
| ------------- |:-------------:|
| 强引用(Strong) | ✔️ | ✔️|✔️|✔️|
| 弱引用(Weak) | ✔️ | ❌|✔️|❌|
| 无主引用(Unowned) | ✔️ | ✔️|❌|✔️|

我们用一段代码来演示 unowned 关键字使用的情景。假设我们现在有两个类,一个 Customer 类,和一个 CreditCard 类。我们知道,一张信用卡一定有一个主人,而一个消费者不一定有一张信用卡。因而我们需要在 Customer 类设计一个 Optional 的信用卡属性,然后在 CreditCard 类设计一个非 Optional 的消费者类。同时我们为了避免强引用循环的产生,我们把 CreditCard 类中的消费者属性设计为 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") }

}

然后我们创建两个对象,看一看控制台是否正常输出:

do {
    let cus1 = Customer(name: "Kenneth")
    let cus2 = CreditCard(number: 6222136579841254, customer: cus1)
}
控制台

在这种情况下,CreditCard 类和 Person 类相互引用了对方,但是因为我们把 CreditCard 类中的 customer 属性设计成 unowned,就不会出现强引用循环。

闭包中的循环引用

TBD...

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

推荐阅读更多精彩内容

  • 作为一门现代的高级编程语言,Swift代替我们进行了对象的创建和销毁等相关的内存管理。它使用了一个优雅的技术,叫做...
    Maru阅读 2,086评论 4 17
  • 29.理解引用计数 Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数...
    Code_Ninja阅读 1,469评论 1 3
  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 1,946评论 1 16
  • 内存管理 ARC处理原理 ARC是Objective-C编译器的特性,而不是运行时特性或者垃圾回收机制,ARC所做...
    b485c88ab697阅读 11,180评论 3 47
  • 结缘美乐家是在去武晔姐的办公室,当时我的工作事业走下坡路即于清零最迷茫的时候整个人精神状态都是一种空洞。一头长发干...
    爱阿臻阅读 396评论 1 0