Swift 自动引用计数器(ARC)详解

2018-08-17  本文已影响0人  WSJay

Swift使用ARC(自动引用计数器:Automatic Reference Counting)来追踪和管理应用的内存使用。在大多数情况下,你不需要自己考虑内存管理问题,ARC会自动释放那些不需要的类实例所占用的内存。引用计数仅适用于类的实例, 结构和枚举是值类型,而不是引用类型,不会通过引用被存储和传递。

一、 ARC工作原理

每次创建一个类的新实例时,ARC都会分配一块内存来存储该实例的信息。 这块内存保存有关实例类型的信息,以及与该实例相关联的任何存储属性的值。
此外,当不再需要某个实例时,ARC会释放它占用的内存,以便这块内存可以用于其他目的。 这确保类实例在不需要时不会占用内存空间。
但是,如果ARC要释放仍在使用中的实例,将无法再访问该实例的属性,或者调用该实例的方法。 事实上,如果你试图访问该实例,你的应用程序很可能会崩溃。
为了确保实例在使用时不被销毁,ARC会跟踪每个类实例当前有多少属性、常量和变量引用。只要有一个对该实例的引用仍然存在,则ARC就不会释放该实例。
为了实现这一点,每当将一个类实例分配给一个属性,常量或变量时,该属性,常量或变量将强制引用该实例。 这个引用称为“强”引用,只要强引用存在,ARC就不会释放该实例。

二、ARC的应用

这是一个自动引用计数器如何工作的示例。 这个例子从一个简单的类Person开始,它定义一个名为name的常量存储属性:

class Person {
    
    //存储属性name
    let name: String

    //使用初始化方法设置name的初始值(也可设置为可选类型,默认初始值为nil)
    init(name: String) {
    self.name = name
    print("\(name) is being initialized")
    }
    //当Person实例被销毁时自动调用该方法
    deinit { print("\(name) is being deinitialized")}
}

下面的代码定义了三个Person类型的变量,用于设置对一个新的Person实例的多个引用。 因为这些变量是可选类型(Person ?,而不是Person),所以它们将自动被初始化为nil,并且当前不引用Person实例。

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

现在,创建一个新的Person实例并将其分配给以下三个变量之一:

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

注意,“John Appleseed is being initialized”消息是在调用Person类的构造器时打印的。这证实已经进行了初始化。
因为将新的Person实例分配给reference1变量,所以现在有一个从reference1到新的Person实例的强引用。 因为至少有一个强引用存在,所以ARC确保这个Person在内存中不会被释放。
如果将同一个Person实例分配给另外两个变量,则会创建两个强引用,现在对单个Person实例有三个强引用:

reference2 = reference1
reference3 = reference1

如果通过将其中两个变量设置为ni l来断开两个强引用(包括原始引用 reference1),那么单个强引用仍然保留,并且Person实例不会被释放:

reference1 = nil

//此时不会调用deinit方法,直到最后的强引用断开(reference3),ARC才会释放Person实例
reference2 = nil

当最后一个强引用被断开时,系统才会知道你不再使用Person实例,此时ARC才会释放它。

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

三、类实例之间的强引用循环

在上面的示例中,ARC能够跟踪创建的新Person实例的引用数,并在不需要该Person实例时释放它。
如果两个类实例之间彼此保持强引用,使得每个实例保持另一个实例存活, 这称为强引用循环。通过将类之间的一些关系定义为weak引用或unowned引用(后面会详细介绍unowned),而不是强引用,可以解决强引用循环问题。 但是,在学习如何解决强引用循环问题之前,让我们先了解强引用循环是如何引起的。

下面的示例将说明如何创建强引用循环,首先定义两个类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") }

}

每个Person实例都有一个类型为String的name属性,并且有一个可选的apartment属性,初始值为nil。apartment属性是可选的,因为Person也许不会一直有一个Apartment。类似地,每个Apartment实例都有一个String类型的unit属性,并且有一个可选的tenant属性,初始值为nil。tenant 属性是可选的,因为Apartment也许不会一直有一个Person

然后我们定义两个可选类型的变量johnunit4A,并创建一个特定的Person实例和Apartment实例,并将它们分配给johnunit4A变量:

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

john变量现在具有对新的Person实例的强引用,并且unit4A变量具有对新的Apartment实例的强引用,其关系如下图(referenceCycle01)所示:

referenceCycle01.png

最后将两个实例关联起来,感叹号(!)用于解包和访问存储在john和unit4A可选变量中的实例,以便可以设置这些实例的属性(强制解析)。Optional Chaining

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

下面是强引用如何将两个实例链接在一起:

referenceCycle02.png
然而链接这两个实例就在它们之间创建了一个强引用循环。
Person实例现在具有对Apartment实例的强引用,并且Apartment实例具有对Person实例的强引用。因此,当您破坏johnunit4A变量所持有的强引用时,引用计数不会减为零,实例也不会被ARC重新分配:
john = nil
unit4A = nil

当将这两个变量设置为nil时,两个实例对象都未调用各自的析构器。强引用循环阻止了Person和Apartment实例被释放,从而在应用程序中导致内存泄漏。将john和unit4A变量设置为nil之后,实例之间的关系如下图所示:

referenceCycle03.png

Person实例和Apartment实例之间的强引用仍然存在,并没有被断开。

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

当使用类类型的属性时,Swift提供了两种方法来解决强引用循环:weak引用和unowned引用。
weakunowned使一个引用循环中的一个实例引用另一个实例,而不保持强引用。 这样实例之间可以彼此引用而不会产生强引用循环问题。
当另一个实例的生命周期较短时,即当另一个实例可以首先被释放时,使用weak引用。相反,当另一个实例有相同的生命周期或更长的生命周期时,使用unowned引用。

(一)弱引用(weak)

Weak不会对引用的实例保持强引用,因此不会阻止ARC销毁引用的实例。这样就可以避免强引用循环。通过将关键字weak放在属性或变量声明之前来表明弱引用。

因为弱引用不会对其引用的实例保持强引用,所以该实例有可能在弱引用仍然存在时就被释放了。当它所引用的实例被释放时,ARC会自动将弱引用设置为nil。当ARC将弱引用设置为nil时,属性观察器不会被调用。并且,因为弱引用需要允许它们的值在运行时被设置为nil,所以它们总是声明为可选类型的变量,而不是常量。

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

现在我们使用弱引用来解决上面的强引用循环问题,将Apartment中的tenant属性声明为weak:


class Person {

let name: String
init(name: String) { self.name = name }

//Person对Apartment是强引用
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }

}

class Apartment {

let unit: String
init(unit: String) { self.unit = unit }

//Apartment在某时刻可以没有Person是合理的,因此将其设置为weak来解决强引用循环问题
//Person在运行时可能为nil,因此将其设置为可选类型的变量
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }

}

来自两个变量(johnunit4A)的强引用和两个实例之间的链接如前所述创建:


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

下面是链接这两个实例后的引用:

weakReference01_2x.png

Person实例仍然具有对Apartment实例的强引用,但是Apartment实例现在具有对Person实例的弱引用。 这意味着当你通过将john变量设置为nil来断开它的强引用时,就没有对Person实例的更多强引用:


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

因为对Person实例没有更多的强引用,它被释放,并且tenant属性设置为nil,如下图所示:

weakReference02_2x.png

唯一剩下的对Apartment实例的强引用来自unit4A变量。 如果断开对Apartment实例的强引用,则没有对Apartment实例的更多强引用。

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

因为不再有对Apartment实例的强引用,所以它也被释放了:

weakReference03_2x.png

注意:在使用垃圾回收的系统中,弱指针有时用于实现简单的缓存机制,因为只有当内存压力触发垃圾回收时,才会释放没有强引用的对象。然而,对于ARC,只在其最后一个强引用被删除后立即被释放,这使得弱引用不适合这样做。

(二) unowned引用

像弱引用一样,unowned引用也不会对它所引用的实例保持强引用。然而,与弱引用不同,当另一个实例有相同的生命周期或更长的生命周期时使用unowned引用。通过将关键字unowned放在属性或变量声明之前,来指明它是一个unowned引用。
一个unowned引用应该总是有一个值。因此,ARC永远不会将unowned引用的值设置为nil,这意味着使用非可选类型定义unowned引用。

注意: 只有当您确定这个引用总是引用着一个还未被释放的实例时,才使用unowned引用。
如果试图在实例被释放后访问unowned引用的值,将得到一个运行时错误。

因为unowned引用是非可选的,所以每次使用它时,不需要解包,可以直接访问。但是,当它所引用的实例被释放时,ARC不能将它设置为nil,因为非可选类型的变量不能设置为nil。另外,也不可再访问该引用,否则会触发一些运行时错误。只有确定一个引用始终引用一个实例时,才使用unowned 引用。

下面的示例定义了两个类,CustomerCreditCard,它们对银行客户和该客户可能使用的信用卡进行建模。这两个类都将另一个类的实例存储为属性。这种关系有可能创建一个强引用循环。
CustomerCreditCard的关系与上面弱引用示例中Apartment(公寓) 和Person 的关系略有不同。在这个数据模型中,客户可能拥有信用卡,也可能没有,但是信用卡总是与客户相关联的。一个CreditCard实例的生命周期永远不会超过它所引用的客户。为了表示这一点,Customer类有一个可选的card属性,但是CreditCard类有一个unowned的和非可选的Customer属性。
此外,只能通过将一number的值和一个customer实例传递给自定义的CreditCard构造器来创建新的CreditCard实例。这确保在创建CreditCard实例时,CreditCard实例总是有一个与之关联的customer实例。
因为信用卡总是会有一个客户,所以将用unowned修饰它的customer属性,以避免强引用循环:

class Customer {
    let name: String
  //每个客户可以持有或不持有信用卡,所以将属性card定义可选类型的变量
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
  /*
     1.每张信用卡总有个客户与之对应,与每张信用卡相关联的客户不能为空,而且不能更换,因此将customer属性定义为非可选的常量;
     2.由于信用卡始终拥有客户,为了避免强引用循环问题,所以将客户属性定义为unowned
     */
    unowned let customer: Customer
 // 只能通过向初始化方法传递number和customer来创建CreditCard实例,确保CreditCard实例始终具有与其关联
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

注意:该示例中的类Customer(客户)和CreditCard(信用卡)之间的关系与弱引用示例中的Apartment(公寓)和 Person(人)之间的关系略微不同。

下一个代码片段定义了一个名为john的可选类型Customer变量,该变量将用于存储对特定客户的引用。 由于是可选的,该变量的初始值为nil:

var john: Customer?

现在可以创建一个Customer实例,并使用它初始化和分配一个新的CreditCard实例作为该客户的card属性:

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

下面是引用的关系,现在已经链接了两个实例:

unownedReference01_2x.png
现在Customer的实例对CreditCard的实例是强引用,而CreditCard的实例对Customer的实例是unowned引用。
由于customer属性是unowned引用,所以当断开john变量对Customer实例的强引用后,则没有对Customer实例的强引用了:
unownedReference02_2x.png

因为没有对Customer实例的强引用,所以它被释放。Customer实例被释放之后,就没有对CreditCard实例的强引用,CreditCard实例也被释放。

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

上面的最后一个代码片段显示,在john变量设置为nil之后,Customer实例和CreditCard实例都打印了它们被销毁的消息。

(三) unowned引用和隐式解包的可选属性

上面weakunowned引用中的两个示例涵盖了两种常见的情况,它们都是为了解决强引用循环问题。
weak引用中的PersonApartment例子描述了一种情况:两个类相互引用时,将彼此作为自己的属性,且这两个属性都允许为nil,此时有可能会造成强引用循环问题。这种情况最好用weak来解决。unowned引用中的CustomerCreditCard 例子描述了另外一种情况:两个类相互引用时,将彼此作为自己的属性,且一个属性可以为nil,而另外一个属性不可为nil,此时也有可能会造成强引用循环问题。这种情况最好用unowned来解决。
但是,还有第三种情况,其中两个属性应该总是有一个值,并且一旦初始化完成后,这两个属性都不应为nil。 在这种情况下,将一个类的属性设置为unowned,另一个类的属性设置为隐式可选类型(!)。这使得两个属性在初始化完成后可以直接访问(没有可选的解包),同时避免了引用循环问题。
让我们看看如何解决第三种情况造成的引用循环问题:下面的示例定义了两个类:CountryCity,每个类都将另一个类的实例存储为属性。在这个数据模型中,每个国家都必须有一个首都,每个城市都必须属于一个国家。为了表示这一点,Country类中有一个capitalCity属性,而City类中有一个country属性:

class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
     // 在Country的初始化方法中来创建City实例,并将此实例存储在其capitalCity属性中
     //在Country的初始化方法中调用City的初始化方法。 但是,只有完全初始化一个新的Country实例后,才不能将self传递到City初始化器中
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    //City的初始化器使用一个Country实例,并将此实例存储在其country属性中
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

为了在两个类之间建立相互依赖关系,City的构造器获取一个Country实例,并将该实例存储在其country属性中。

City的构造器是在Country的构造器内调用的。然而,只有当一个新的Country实例完全初始化之后,Country的构造器才能将self传递给City的构造器。(两阶段初始化
为了满足该要求,将CountrycapitalCity属性声明为隐式解包的可选属性,即City!。这意味着capitalCity属性的默认值为nil,与其他任何可选值一样,但可以在不需要解包的情况下访问其值。(隐式解包选项
因为capitalCity具有默认值nil,所以只要Country实例在其构造器中设置name属性,新的Country实例就认为被完全初始化。这意味着Country构造器可以设置在name属性后就可以开始引用和传递隐式self属性。因此,当Country构造器正在设置其capitalCity属性时,Country构造器可以传递self作为City构造器的参数之一。

这意味着可以在单个语句中创建CountryCity实例,也不会创建强引用循环,而且可以直接访问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"

在上面的示例中,使用隐式解包可选方法意味着满足所有两阶段类构造器需求(Two-Phase Initialization)。在初始化完成后,capitalCity属性可以像非可选值一样使用和访问,同时避免了强引用循环。

三、闭包的强引用循环

如果将闭包分配给类实例的属性,并且该闭包的主体捕获该实例,则也可能发生强引用循环。这个捕获可能发生,因为闭包的主体访问实例的属性,比如self.someProperty。或者因为闭包调用实例的方法,比如self.someMethod()。在任何一种情况下,这些访问都会导致闭包“捕获”self,从而创建一个强引用循环。
之所以会出现这种强引用循环,是因为闭包和类一样都是引用类型。当你将闭包分配给一个属性时,就增加了一个对该闭包的引用。实质上,它与上面的问题相同 - 两个强引用相互保持对方存活。但是,这次是一个类实例和一个闭包,而不是两个类实例。
Swift为这个问题提供了一个优雅的解决方案,称为闭包捕获列表。然而,在学习如何使用闭包捕获列表来打破强引用循环之前,首先来了解导致这种循环引用的原因。
下面的示例展示了在使用引用self的闭包时,如何创建强引用循环。这个例子定义了一个名为HTMLElement的类,它为HTML文档中的单个元素提供了一个简单的模型:

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属性,可选的text属性,还有懒加载属性asHTML 。默认情况下,asHTML属性被分配一个闭包。

例如,可以将asHTML属性设置为一个闭包,如果text属性为nil,该闭包返回默认值,以防止表示返回空的HTML标记:

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类创建和打印新实例的方法:

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

不幸的是,如上所述,HTMLElement类在其实例和用于asHTML默认值的闭包之间创建了一个强引用循环。如下图所示:

closureReferenceCycle01_2x.png

注意:即使闭包多次引用self,它也只捕获对HTMLElement实例的一个强引用。

如果将paragraph变量设置为nil并且断开其对HTMLElement实例的强引用,则由于强引用循环,HTMLElement实例及其闭包都不会被释放:

paragraph = nil

注意HTMLElement析构器中的消息没有被打印出来,这表明HTMLElement实例没有被释放。

四、解决闭包的强引用循环

通过将捕获列表作为闭包定义的一部分,这可以解决闭包和类实例之间的强引用循环。捕获列表定义了在闭包主体中捕获一个或多个引用类型时要使用的规则。与两个类实例之间的强引用循环一样,将每个捕获的引用声明为weak引用或unowned引用,而不是强引用。至于选择weak引用,还是unowned引用,取决于代码不同部分之间的关系。

(一)定义捕获列表

捕获列表中的每一项都是weakunowned关键字与类实例(例如self)引用的配对,或者是与用某个值初始化的变量的配对(例如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
}

如果闭包没有指定参数列表或返回类型,因为它们可以从上下文中推断出来,请将捕获列表放在闭包的最开始位置,后面跟着in关键字:

lazy var someClosure: () -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // closure body goes here
}
(二)weak 和 unowned 引用

当闭包和它捕获的实例总是相互引用,并且总是在同一时间被释放时,在闭包中定义一个unowned引用。
相反,当捕获的引用在将来的某个时刻可能变成nil时,将捕获定义为weak引用。weak引用总是可选类型,当它们引用的实例被释放时,它会自动变成nil。这使你能够检查它们是否存在于闭包的主体中。

注意:如果捕获的引用永远不会变成nil,那么它应该总是被捕获为一个unowned引用,而不是一个weak引用。

unowned引用是用于解决HTMLElement示例中强引用循环的适当捕获方法,该方法来自上面的Closures的强引用循环。 以下是编写HTMLElement类以避免循环的方法:

unowned引用是用于从上面的闭包的强引用循环中解析HTMLElement示例中的强引用周期的适当捕获方法。下面是如何编写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")
    }

}

除了在asHTML闭包中添加捕获列表之外,HTMLElement的这种实现与先前的实现相同。 在这种情况下,捕获列表是[unowned self],这意味着将self捕获为unowned用而不是强引用
可以像以前一样创建和打印HTMLElement实例:

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

下面是使用捕获列表时引用关系图:

closureReferenceCycle02_2x.png
这一次,闭包对self的捕获是一个unowned引用,并没有对它捕获的HTMLElement实例保持强引用。 如果将paragraph变量中的强引用设置为nil,则会释放HTMLElement实例,这可以从下面示例中的析构器消息的打印中看出:
paragraph = nil
// Prints "p is being deinitialized"

有关捕获列表的更多信息,请查看Capture Lists

五、其他专题模块

Swift 4.2 基础专题详解

上一篇下一篇

猜你喜欢

热点阅读