ARC内存管理以及循环引用

ARC:"Automatic Reference Counting",自动引用计数。Swift语言延续了OC的作法,也是利用ARC机制进行内存管理,和OC的ARC同样,当一些类的实例不在须要的时候,ARC会释放它们的内存。可是,在少数状况下,ARC须要知道你的代码之间的关系才能更好的为你管理内存,和OC同样,Swift中的ARC也存在循环引用致使内存泄露的状况。html

1、ARC的工做机制

每当咱们建立一个类的新的实例的时候,ARC会从堆中分配一块内存用来存储有关该实例的信息。这块内存将持有这个实例的类型信息以及和它关联的属性的值。另外,当这个实例再也不被须要的时候,ARC将回收这个实例所占有的内存而且将这部份内存给其余须要的实例用。这样就能保证再也不被须要的实例不占用多余的内存。 可是,若是ARC释放了正在使用的实例,那么该实例的属性将不能被访问,方法将不能被调用,若是你访问它的属性或者调用它的方法时,应用会崩溃,由于你访问了一个野指针。 为了解决上述问题,ARC会跟踪每一个类的实例正在被多少个属性、常量或者变量引用,每当你将类实例赋值给属性,常量或者变量的时候它就会被"强"引用一次,当它的引用计数为0时,代表它再也不被须要,ARC就会销毁它。 下面举个例子介绍ARC是如何工做的bash

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类的实例。app

reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
复制代码

如今建立一个Person类的实例,而且赋值给reference1,此时控制台会打印"John Appleseed is being initialized"函数

reference2 = reference1
reference3 = reference1
复制代码

而后将该实例赋值给reference2reference3。如今该实例被三个"强"类型的指针引用。ui

reference1 = nil
reference2 = nil
复制代码

如上所示,当咱们将其中两个引用赋值给nil的时候,这两个"强"引用被打破,可是这个Person的实例并无被释放(释放信息未打印),由于还存在一个对这个实例的强引用。spa

reference3 = nil
// Prints "John Appleseed is being deinitialized"
复制代码

当咱们将第三个"强"引用打破的时候(赋值为nil),能够看到控制台打印的"John Appleseed is being deinitialized"析构信息。3d

2、两个类实例之间的循环引用

上述的例子中,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方法,而且打印一段信息,用来让咱们清楚这个实例什么时候被销毁。code

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,它们占用的内存仍是得不到释放,所以就会形成内存泄露。

3、解决两个类实例之间的循环引用

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"
复制代码

而后将变量 unit4A设为 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修饰的变量必需要指向一个实例(形成循环引用的两实例的生命周期差很少长,不会出现一方被提早释放的状况),一旦它被释放了,就千万别再使用了。

4、闭包引发的循环引用

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分析获得的循环引用以及形成的内存泄漏。

5、使用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

相关文章
相关标签/搜索