Swift ARC内存管理以及循环引用

ARC:"Automatic Reference Counting",自动引用计数。Swift语言延续了OC的做法,也是利用ARC机制进行内存管理,和OC的ARC一样,当一些类的实例不在需要的时候,ARC会释放它们的内存。但是,在少数情况下,ARC需要知道你的代码之间的关系才能更好的为你管理内存,和OC一样,Swift中的ARC也存在循环引用导致内存泄露的情况。
一、ARC的工作机制
每当我们创建一个类的新的实例的时候,ARC会从堆中分配一块内存用来存储有关该实例的信息。这块内存将持有这个实例的类型信息以及和它关联的属性的值。另外,当这个实例不再被需要的时候,ARC将回收这个实例所占有的内存并且将这部分内存给其他需要的实例用。这样就能保证不再被需要的实例不占用多余的内存。
但是,如果ARC释放了正在使用的实例,那么该实例的属性将不能被访问,方法将不能被调用,如果你访问它的属性或者调用它的方法时,应用会崩溃,因为你访问了一个野指针。
为了解决上述问题,ARC会跟踪每个类的实例正在被多少个属性、常量或者变量引用,每当你将类实例赋值给属性,常量或者变量的时候它就会被"强"引用一次,当它的引用计数为0时,表明它不再被需要,ARC就会销毁它。
下面举个例子介绍ARC是如何工作的

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

上述代码创建了一个名为Person的类,该类声明了一个非可选的类型的name常量,一个给name赋值的初始化方法,并且打印了一句话,用来标注初始化成功,同时声明了一个析构函数,打印了一句标志此实例被销毁的信息。

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

上述代码声明了三个Person?类型的变量,这三个变量为可选类型,所以被自动初始化为nil,此时三个实例都没有指向任何一个Person类的实例。

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

现在创建一个Person类的实例,并且赋值给reference1,此时控制台会打印"John Appleseed is being initialized"

reference2 = reference1
reference3 = reference1

然后将该实例赋值给reference2reference3。现在该实例被三个"强"类型的指针引用。

reference1 = nil
reference2 = nil

如上所示,当我们将其中两个引用赋值给nil的时候,这两个"强"引用被打破,但是这个Person的实例并没有被释放(释放信息未打印),因为还存在一个对这个实例的强引用。

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

当我们将第三个"强"引用打破的时候(赋值为nil),可以看到控制台打印的"John Appleseed is being deinitialized"析构信息。
二、两个类实例之间的循环引用
上述的例子中,ARC可以很好的获取一个实例的引用计数,并且当它的引用计数为0的时候释放它。但是在实际的开发过程中,会存在一些特殊情况,使ARC没办法得到引用计数为0这个关键点,就会造成这个实例的内存一直不被释放,两个类的实例相互"强"引用就会造成这种情况,就是"循环引用"。
苹果官方提供了两种方法来解决两个实例之间的循环引用,unowned引用和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 }
    var tenant: Person?
    deinit { print("Apartment \(unit) is being deinitialized") }
}

这个例子,定义了一个Person类和一个Apartment类。每一个Person的实例都有一个name的属性和一个apartment的可选属性,初始化为nil,因为并不是每一个人都拥有一个公寓,所以是可选属性。同样的,每一个Apartment实例都有一个unit属性和一个tenant的可选属性,初始化为nil,同理,不是每一个公寓都有人租。同时,两个类都定义了deinit方法,并且打印一段信息,用来让我们清楚这个实例何时被销毁。

var john: Person?
var unit4A: Apartment?

分别定义一个Person类型和Apartment的变量,定义为optional(可选类型),初始化为nil

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

然后分别创建一个Person类的实例和Apartment类的实例,并且分别赋值给上面的定义的变量。


上图为此时变量和实例之间的强引用关系。
然后john将拥有一座公寓unit4A,公寓unit4A将被john承租。

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

因为可以确定两个变量都被赋值为相应类型的实例,所以此处用!对可选属性强解包。
此时,两个变量和实例以及两个实例之间的"强"引用关系如下图。


从图中可以看到两个实例互相"强"引用,也就是说这两个实例的引用计数永远不会为0,ARC也不会释放这两个实例的内存。

john = nil
unit4A = nil

当我们将两个变量设置为nil,切断他们与实例之间的"强"引用关系,此时两个实例之间的"强"引用关系为:


从图中可以看出,这两个实例的引用计数仍然不为0,它们占用的内存还是得不到释放,因此就会造成内存泄露。
三、解决两个类实例之间的循环引用
Swift提供了两种办法解决类实例之间的循环引用。weak引用个unowned引用。这两种方法都可以使一个实例引用另一个实例的时候,不用保持"强"引用。weak一般应用于其中一个实例具有更短的生命周期,或者可以随时设置为nil的情况下;unowned用于两个实例具有差不多长的生命周期,或者说两个实例都不能被设置为nil
(1)weak引用
weak引用对所引用的实例不会保持"强"引用的关系。假如一个实例同时被若干个"强引用"和一个weak引用引用时,当所有其他的"强"引用都被打破时该实例就会被ARC释放,并且ARC会自动将这个weak引用置为nil。因此,weak引用一般被声明为var,因为它会被ARC设置为nil

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

现在,我们将Apartment类中的tenant变量声明为weak引用(在var关键字前加weak关键字),表明某公寓的承租人并不一定一直都是同一个人。

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

然后和上文一样,将两个变量和实例关联。此时,它们之间的引用关系如下图。


Person实例仍然"强"引用Apartment实例,但是Apartment实例weak引用Person实例。johnunit4A两个变量仍然"强"引用两个实例。当我们把john变量对Person实例的"强"引用打破的时候,即将john设置为nil,就没有其他的"强"引用引用Person实例,此时,Person实例被ARC释放,同时Apartment实例的tenant变量被设置为nil

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


然后将变量ubit4A设为nil,可以看到Apartment实例也被销毁。

unit4A = nil
// Prints "Apartment 4A is being deinitialized"


(2)unowned引用
weak引用一样,unowned引用也不会保持它和它所引用实例之间的"强"引用关系,而是保持一种非拥有(或未知)的关系,使用的时候也是用unowned关键字修饰声明的变量。不同的是,两个互相引用的对象具有差不多长的生命周期,而不是其中一个可以提前被释放(weak),有点患难与共的意思。
Swift要求unowned修饰的变量必须一直指向一个实例,而不是有些时候为nil,因此,ARC也不会将这个变量设置为nil,所以我们一般将这个引用声明为非可选类型。PS:请确保你声明的变量一直指向一个实例,如果这个实例被释放了,而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") }
}

上面这个例子定义了两个类:CustomerCreditCard,每个顾客都可能会有一张信用卡(可选类型),每个信用卡都一定会有一个持有他们的顾客(非可选类型,卡片为顾客定制)。因此,Customer类有一个CreditCard?类型的属性,CreditCard类也有一个Customer类型的属性,并且被声明为unowned,以此来打破循环引用。每张信用卡初始化的时候都需要一名持有它的顾客,因为信用卡本身就是为顾客定制的。

var john: Customer?

然后声明一个Customer?类型的变量john,初始化为nil。接着创建一个Customer的实例,并且将它赋值给john(让john引用它、指向它都是一个意思)。

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

(第一句代码赋值之后,我们知道john肯定不是nil,所以用!解包不会有问题)
然后,两个实例之间的引用关系为:


Customer实例"强"引用CreditCard实例,CreditCard实例unowned引用Customer实例,接着,我们将johnCustomer实例的"强"引用打破,即将john设置为nil

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


可以看到Customer实例和CreditCard实例都被销毁了。john被设置为nil之后,就没有"强"引用引用Customer实例,所以,Customer实例被释放,也就没有"强"引用引用CreditCard实例,因此CreditCard实例也被释放。
以上例子证明,两种方式都可以解决循环引用的问题,但是要注意它们使用的范围。weak修饰的变量可以被设置为nil(引用的实例的生命周期短于另一个实例),unowned修饰的变量必须要指向一个实例(造成循环引用的两实例的生命周期差不多长,不会出现一方被提前释放的情况),一旦它被释放了,就千万别再使用了。
四、闭包引起的循环引用
Swift中的闭包是一种独立的函数代码块,它可以像一个类的实例一样在代码中赋值、调用和传递,也可以被认为某个匿名函数的实例,其实就是OC中的block。它和类一样也是引用类型的,所以它的函数体中使用的引用都是"强"引用。

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

上述例子中,闭包被赋值给asHTML变量,所以闭包被HTMLElement实例"强"引用,而闭包又捕获(关于闭包捕获变量,参考官方文档Capturing Values)了HTMLElement的实例中的textname属性,因此它又"强"引用HTMLElement实例,这样就造成了循环引用,因为text属性可能为空,所以定义为可选属性。

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

我们创建一个HTMLElement实例,并将它赋值给paragraph变量,然后访问它的asHTML属性。此时的内存示例为下图,可以看到HTMLElement实例和闭包之间的循环引用。


当我们将paragraph 设置为nil时,控制台并没有打印任何销毁信息,因为循环引用。

上图为使用Instruments分析得到的循环引用以及造成的内存泄漏。
五、使用unowned和weak解决循环引用
通过上文(三)的分析,我们知道unowned引用对实例的非拥有关系,因此,我们可以通过如下方式解决循环引用:

lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

[unowned self] in,这段代码,代表闭包中的self指针都被unowned修饰。这样就可以使闭包对实例的"强"引用变成unowned引用,从而打破循环引用。
当HTML的element为标题的时候,此时如果text属性为空,我们想返回一个默认的text作为标题,而不是只有<h/>这种标签。

let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"

这段代码也会造成HTMLElement对其自身的循环引用。我们仍然可以使用unowned关键字打破循环引用:

heading.asHTML = {
    [unowned heading] in
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"

unowned会使闭包中对heading的"强"都改为unowned引用。
或者,可以使用weak属性打破循环引用:

weak var weakHeading = heading
heading.asHTML = {
    return "<\(weakHeading!.name)>\(weakHeading!.text ?? defaultText)</\(weakHeading!.name)>"
}
// Prints "<h1>some default text</h1>"
//Prints "h1 is being deinitialized"

上文(三)中可知,weak修饰的变量为可选类型,而且,我们对变量进行了一次赋值,就可以确保weakHeading指向heading引用的实例,所以可以放心的使用!对它解包。
上面这段代码同样可以使闭包对HTMLElement实例的"强"引用变为weak引用,从而打破循环引用。
(ARC会自动回收不被使用的对象,所以不用手动将变量设置为nil
本文参考Automatic Reference Counting

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

推荐阅读更多精彩内容