Swift 自动引用计数(Automatic Reference Counting, ARC)

自动引用计数(Automatic Reference Counting, ARC)

Swift使用自动引用计数来跟踪和管理你的app的内存使用。在大多数情况下,Swift帮你做好了内存管理工作,你不需要自己手动管理内存。当一些类实例不再被使用的时候,ARC自动释放这些实例所占用的内存。

然而,在某些特殊的情况下,ARC为了更好管理内存需要知道更多关于你代码之间的关系。在这一章节里,将会看到这些情况下,我们如何利用ARC来更好地管理app中的所有内存。
注:ARC只对类实例有用,对于结构体和枚举类型这样的值类型,是不需要引用的。

ARC的工作机制

每次你创建一个实例的时候,ARC就会为你创建一块内存来存储类相关的信息,这些信息包含了实例的类型,和存储属性中的值。
当不再需要这些实例的时候,ARC就会释放这些内存,以供其他的目的使用。这样的话这些类不再使用的时候就不会再占用内存空间,从而避免内存泄漏。

如果你的实例仍在使用的时候ARC回收了这个实例的内存,你再访问这个实例的属性和调用这个实例的方法的时候,那么将会导致你的应用崩溃。为了保证你的实例在使用的时候不会被回收,ARC会跟踪每个类实例被多少个属性、常量和变量所引用。只要一个实例至少有一个引用,那么ARC就不会对这个实例进行回收。因此,当你将一个实例赋给变量、常量或者属性时,这些常量、变量或者属性就会对这个实例有一个强引用(Strong Reference),只要有强引用的存在,那么这个实例就不会被回收。

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和一个初始化器以及一个析构器。当创建一个Person类实例的时候,会打印一些信息,当这个Person类实例被回收的时候,析构器也会打印出一些信息。

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

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

当创建了Person类实例的时候,就打印出了”John Appleseed is being initialized”这个信息。而且将其赋给了reference1,现在reference1就持有这个实例的强引用。所以ARC不会回收这个实例。

reference2 = reference1
reference3 = reference1

接着讲这个实例又赋给了reference2reference3,现在对于这个实例已经有三个强引用了。如果释放掉其中的两个强引用,如下:

reference1 = nil
reference2 = nil

那么现在还有一个变量reference3持有这个实例的强引用,所以该实例不会被回收。

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

当最后的一个强引用被释放,如上。则这个实例目前强引用的数量是0,所以这个实例会被ARC回收,那么析构器会被调用,就会打印出”John Appleseed is being deinitialized”。

类实例之间循环强引用

以上的例子中创建了Person类实例,以及当Person类实例的强引用解除之后ARC将这个实例回收。但是,在某些情况下,则可能会产生循环强引用,例如一个类的实例A强引用另一个类是实例B,同时B强引用A。这样A和B实例将得不到释放,因为始终有强引用持有他们。

为了解决类之间的循环强引用问题,只能不采用强引用这种关系,可以采用弱引用(Weak Reference)和无主引用(Unowned Reference)来代替强引用。

在解决这个问题之前,先用一个关于循环强引用的简单例子来说明循环强引用。

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实例都有一个name属性和一个apartment可选类型,它的默认值为nil,分别代表这个人的名字和他所住的公寓,当然这个人可能没有公寓,所以公寓这个属性是可选的。每一个Apartment实例都有一个unit属性、tenant可选类型属性,分别表示公寓所在的单元号和租客,因为并不是每个公寓都有租客,所以租客为可选类型。

var john: Person?
var unit4A: Apartment?

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

以上定义了两个可选类型,并生成两个实例赋给它们,执行完以上代码之后内存的引用关系如下图。

john持有Person实例的强引用,而init4A持有Apartment实例的强引用。现在把这两个实例连在一起。

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

现在内存中的引用关系如下图。

johninit4A分别持有对方的强引用,这就形成了循环强引用。当释放掉johnunit4A的强引用之后,Person实例和Apartment实例的引用计数被不会降为0,所以ARC不会对这两个实例进行回收,那么就存在内存泄漏了。

john = nil
unit4A = nil

现在内存中的引用关系如下图所示。

这两个实例分别持有对方的强引用,而且这两个实例的关系不会被打破,更糟的是,这两个实例永远都访问不了,就这样存在内存中。

解决类实例之间的循环强引用

Swift提供了两种方法来解决循环强引用问题:弱引用和无主引用。
弱引用和无主引用能够让循环引用中一个实例去引用另一个实例而不用强引用来持有它,这样这两个实例都可以引用彼此而不用创建一个循环强引用了。
那什么时候该用弱引用什么时候该用无主引用呢?当这个实例在它生命周期任何时候为nil都是合法的,那么就可以考虑用弱引用。当你知道这个实例只要被初始化之后就不会变为nil,那么就可以考虑使用无主引用了。

弱引用

弱引用是一种能够引用实例但是不会对其产生强引用持有的引用,这样就可以防止ARC对实例进行引用计数了。这种表现可以防止引用成为循环强引用的一部分,从而打破循环强引用。讲一个属性或者变量声明为弱引用的话只需要在声明时使用关键字weak即可。

在实例的生命周期里,实例在任何时候都有可能为nil。如果实例一直有值,那么就该考虑使用无主引用。在以上的例子中,一个公寓可以很长一段都没有租客,或者时有租客时而没有,所以可以将Apartment类中的ternent属性设置为弱引用。

因为弱引用不会对实例产生强引用持有,所以很有可能弱引用依旧引用这个实例但是这个实例被ARC回收了。当这个实例被回收之后,ARC会自动将弱引用赋值为nil。因为弱引用需要在运行时可能将其设置为nil,所以一般弱引用都是可选类型变量,而不是常量。所以你在使用的时候,可以像其他可选类型一样来检查这个实例是否存在。

注:当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") }
}

在这里,Apartmeng中的tenant属性声明为弱引用。

var john: Person?
var unit4A: Apartment?

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

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

执行完以上代码之后,内存中的引用关系如下图。

Person实例依旧对Apartment实例保持强引用,但是Apartment实例对Person实例保持弱引用。这样的话,你只要将john设为nil的话,那么就没有其他强引用指向Person实例了,这样这个实例就会被回收了。

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

因为Perosn实例被回收之后,那么tenant属性自动被设置为nil,现在内存中的实例分布如下图所示。

现在内存中唯一的强引用就是unit4A指向Apartment实例,如果打破这个强引用,那么这个实例将会被回收。

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

现在内存中实例分布如下图。

颜色变浅表示这两实例已经被回收了,析构器会打印出相应的信息。

无主引用

和弱引用一样,无主引用也不会对引用的实例产生一个强引用持有。和弱引用不一样的是,无主引用假定了这个引用一直有值。所以,一般无主引用都不能声明为可选类型。你可以用unowned关键字来表明这个属性或者变量是无主引用。

因为无主引用不是可选类型,所以你在使用它的时候无需对其进行解包,无主引用可以直接访问这个实例。但是,当无主引用引用的实例被回收的时候,ARC不会将这个无主引用设置为nil,因为无主引用并非可选类型。

注:如果你尝试用无主引用去访问一个已经被回收的实例,那将会产生运行时错误,导致app崩溃。当你确定这个无主引用一直都会有值的时候你才能去使用这个无主引用。

以下是一个简单的例子,来进一步说明无主引用。

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

以上定义了两个类,一个是Customer,另一个是CreditCard。一个人可以有信用卡,也可以没有信用卡,但是一张信用卡必须有主人。所以在Customer类中将card设置为可选类型,但是在CreditCard中将customer设置为非可选类型,因为要打破循环强引用,所以将customer设置为无主引用。

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

运行以上代码之后,内存中实例和引用关系如下图所示:

john变量对Customer实例持有强引用,实例中的card属性对CreditCard实例持有强引用,同时,CreditCard实例对Customer实例持有一个无主引用。

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

当把Customer实例的强引用断掉之后,Customer实例就再也没有强引用了,所以ARC会对Customer实例进行回收,回收之后,CreditCard实例也没有强引用了,所以CreditCard实例也会被回收。

无主引用和隐式解包可选属性的结合使用

以上两种情况都是比较常见的情景,但是存在着第三种情景,两个属性都必须有值,而且初始化之后这两个属性的值就不能为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
    }
}

以上定义了两个类CountryCity。一个国家必须有一个首都,一个城市必须隶属于一个国家,所以这两个对彼此的引用都是必须有值的。为了打破循环强引用,将Country类中的capitalCity设为隐式解包可选类型,意味这个这个属性的默认值是nil,但是这个属性一旦初始化之后,就要一直保持有值。将City中的country属性设置为无主引用。
Country类的初始化过程中,先对自己的属性都初始化了。因为capitalCity有默认值,所以对name属性初始化之后,初始化第一阶段就完成了,这个时候可以访问属性,调用方法和引用self。所以在实例化City实例的时候可以将self作为实参传入。实例化一个City实例之后,将这个实例赋值给capitalCity属性。

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”

以上的用一条语句创建了两个实例。

闭包循环强引用

以上是两个类实例之间的循环强引用,我们可以使用弱引用和无主引用来打破这种循环强引用。

当我们将一个闭包赋值给一个类实例的属性的时候,因为闭包是引用类型,那么这个属性会对这个闭包产生一个强引用持有,而且当这个闭包体中对这个实例也产生引用的时候,比如访问了这个实例的属性self.property或者调用了这个实例的某个方法,这两种情况都称之为”捕获”self,这个时候就会产生闭包和类实例之间的循环强引用。

为了解决这问题,Swift提供了一种优雅的方式,称之为闭包捕获列表(Closure Capture List)。在讲如何解决这个问题之前,我们先来熟悉一下闭包循环强引用。

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类,该类定义了一个name存储属性,表示这个元素的名字。还定义了一个String可选类型,表示这个HTML元素所渲染的内容。除了这两个属性外,还定义了一个延迟属性asHTML,这个属性引用了一个闭包,而且这个闭包还访问了nametext的两个属性。闭包的类型是()->String,没有参数输入,返回一个String类型。在默认情况下,这个asHTML属性被赋予了一个返回值类型是String的闭包,表示返回这个元素的标签代码,使用这个闭包属性的时候,像使用实例方法一样即可。

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>

注:asHTML属性被定义为lazy属性,因为当你想要写出这个元素的HTML标签代码的时候你才会来
访问这个属性。这个lazy属性意味着你可以在闭包中使用这个self,因为你在访问这个属性的时候就说明整个实例就已经实例化好了。

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

糟糕的是,以上的代码形成了闭包循环强引用。因为实例中的闭包访问了这个实例,而这个实例的属性持有这个闭包的强引用,所以形成了闭包循环强引用。内存中的实例以及实例的引用关系如下图所示。

注:虽然闭包引用多次self,但是对于实例来说只增加了一个强引用。

paragraph的值为nil时:

paragraph = nil

析构器中的信息并没有打印出来,所以这个HTMLElement实例并没有被回收。

注:如果没有访问asTHML属性,让paragraphnil的话,这个实例就会被回收,因为没有形成闭包循环强引用。

解决闭包循环强引用

为了解决闭包循环强引用,我们可以在闭包定义的时候在闭包内部定义一个捕获列表。捕获列表里定义了当在闭包体中捕获一个或者多个引用类型时的一些规则。和类实例之间的循环强引用一样,用弱引用或者无主引用来代替强引用,至于是使用弱引用还是无主引用,取决于代码所表示的实例之间的关系。

注:Swift要求在闭包体中使用self.property或者self.method()来访问属性或者调用方法,而不是直接用property或者method(),主要是为了能够提醒你这里对self产生了一个强引用。

定义捕获列表

捕获列表中的每一项必须由weak或者unowned和一个类实例引用或者一些值来初始化一个变量(如:delegate = self.delegate)所组成,这些项写在一对方括号之中,中间用逗号隔开。将捕获列表放在闭包参数的前面。

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
}

弱引用和无主引用

当闭包和和闭包所捕获的实例将总是引用彼此,并且总同时被回收,这个时候可以将闭包中的捕获定义为无主引用。相反,如果闭包中捕获的实例在某些时刻会变为nil,那么就该将捕获的实例定义为弱引用。而且弱引用总是可选类型的,因为这个实例被回收的时候就将这个引用自动赋值为nil
将以上的例子改为如下:

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

}

除了在闭包中加入一个捕获列表外,其他的全部相同。

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

内存中的实例分布和引用关系如下图所示:

当把paragraph变为nil之后:

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

析构器中的信息就会打印出来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值