23 Automatic Reference Counting

2019-04-06  本文已影响0人  微笑中的你

Swift使用自动引用计数(ARC)来跟踪和管理应用程序的内存使用情况。在大多数情况下,这意味着内存管理在Swift中“只是工作”,您不需要自己考虑内存管理。当不再需要类实例时,ARC自动释放类实例使用的内存。

但是,在一些情况下,ARC需要更多关于代码各部分之间关系的信息,以便为您管理内存。本章将描述这些情况,并展示如何启用ARC管理应用程序的所有内存。在Swift中使用ARC与在Objective-C中使用ARC的方法在过渡到ARC Release Notes中描述的方法非常相似。

引用计数只应用于类的实例。结构和枚举是值类型,而不是引用类型,并且不存储和通过引用传递。

How ARC Works ARC是如何工作的

每次创建类的新实例时,ARC都会分配一块内存来存储该实例的信息。此内存保存有关实例类型的信息,以及与该实例关联的任何存储属性的值。

此外,当不再需要实例时,ARC会释放该实例使用的内存,以便将内存用于其他目的。这确保类实例在不再需要时不会占用内存中的空间。

然而,如果ARC要释放一个仍然在使用的实例,就不再可能访问该实例的属性或调用该实例的方法。事实上,如果您试图访问实例,您的应用程序很可能会崩溃。

为了确保实例不会在仍然需要时消失,ARC跟踪当前引用每个类实例的属性、常量和变量的数量。只要对该实例的至少一个活动引用仍然存在,ARC就不会释放该实例。

为了实现这一点,无论何时将类实例分配给属性、常量或变量,该属性、常量或变量都会对实例进行强引用。这个引用被称为“强”引用,因为它牢牢地抓住了这个实例,并且只要这个强引用仍然存在,就不允许释放它。

ARC in Action ARC运行

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

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?类型的三个变量,用于在后续代码段中设置对新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正在初始化”消息将在调用Person类的初始化器时打印出来。这确认已经进行了初始化。

因为新的Person实例已经分配给了reference1变量,所以现在从reference1到新的Person实例有一个强引用。因为至少有一个强引用,ARC确保这个人被保存在内存中,并且没有被释放。

如果将相同的Person实例分配给另外两个变量,则会建立对该实例的两个强引用:

reference2 = reference1
reference3 = reference1

现在有三个对这个Person实例的强引用。

如果将nil赋值给两个变量,从而破坏了其中的两个强引用(包括原始引用),则仍然保留一个强引用,并且Person实例没有被释放:

reference1 = nil
reference2 = nil

ARC不会释放Person实例,直到第三个也是最后一个强引用被打破,这时很明显你不再使用Person实例:

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

Strong Reference Cycles Between Class Instances 类实例之间的强引用循环

在上面的示例中,ARC能够跟踪对您创建的新Person实例的引用数量,并在不再需要该Person实例时释放该实例。

然而,在编写代码时,类的实例可能永远不会达到没有强引用的程度。如果两个类实例彼此拥有一个强引用,从而每个实例都保持另一个实例为活动的,则会发生这种情况。这就是所谓的强引用循环。

通过将类之间的一些关系定义为弱引用(weak)或无主引用(unowned),而不是强引用,可以解决强引用循环。此过程在解析类实例之间的强引用循环时进行描述。然而,在学习如何解析强引用循环之前,了解这种循环是如何产生的非常有用。

下面是一个如何意外创建强引用循环的例子。这个例子定义了两个类Person和Apartment,它们为公寓及其居民建模:

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属性和一个最初为nil的可选apartment属性。公寓物业是可选的,因为一个人可能并不总是有公寓。

类似地,每个公寓实例都有一个String类型的单元属性,以及一个可选的租户属性,该属性最初为nil。租户财产是可选的,因为公寓可能并不总是有租户。

这两个类还定义了一个反初始化器,它打印出该类的实例正在被反初始化的事实。这使您能够查看Person和Apartment的实例是否按预期重新分配。

下一个代码片段定义了两个可选类型的变量john和unit4A,它们将被设置为下面的特定公寓和Person实例。这两个变量的初始值都为nil,因为它们是可选的:

var john: Person?
var unit4A: Apartment?

您现在可以创建一个特定的Person实例和Apartment实例,并将这些新实例分配给john和unit4A变量:

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

下面是强引用在创建和分配这两个实例之后是如何处理的。
john变量现在对new Person实例有一个强引用,
unit4A变量对new Apartment实例有一个强引用:

现在您可以将这两个实例链接在一起,这样这个人就拥有了一个公寓,而这个公寓又有一个租户。注意感叹号(!)用于打开和访问存储在john和unit4A可选变量中的实例,以便可以设置这些实例的属性:

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

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

不幸的是,链接这两个实例会在它们之间创建一个强引用循环。Person实例现在有一个对公寓实例的强引用,公寓实例也有一个对Person实例的强引用。因此,当您打破john和unit4A变量所持有的强引用时,引用计数不会降为零,实例也不会被ARC释放:

注意,当您将这两个变量设置为nil时,没有调用任何反初始化器。强引用循环防止了Person和Apartment实例被释放,从而导致应用程序的内存泄漏。

下面是强引用是如何处理将john和unit4A变量设置为nil的:


Person实例和公寓实例之间的强引用仍然存在,并且不能被破坏。

Resolving Strong Reference Cycles Between Class Instances 解决类实例之间的强引用循环

在处理类类型属性时,Swift提供了两种解决强引用周期的方法:弱引用(Weak)和无主引用(unowned)。

弱引用和无主引用允许引用循环中的一个实例引用另一个实例,而不需要对它进行强控制。然后,实例可以相互引用,而不需要创建强引用循环。

当另一个实例的生命周期较短时(即可以首先释放另一个实例时),使用弱引用。
在上面的公寓示例中,公寓在其生命周期的某个时间点上没有租户是合适的,因此在本例中,弱引用是打破引用循环的合适方法。相反,当其他实例具有相同的生存期或更长的生存期时,使用无主引用。

Weak References 弱引用

弱引用是一个对它引用的实例没有强持有的引用,因此不会阻止ARC处理引用的实例。这种行为防止引用成为强引用循环的一部分。通过在属性或变量声明前放置weak关键字来指示弱引用。

因为弱引用不会对它引用的实例保持强引用,所以在弱引用仍然引用该实例时,有可能释放该实例。因此,当ARC引用的实例被释放时,它自动将一个弱引用设置为nil。而且,由于弱引用需要允许在运行时将它们的值更改为nil,所以它们总是声明为可选类型的变量,而不是常量。

您可以检查弱引用中是否存在值,就像任何其他可选值一样,并且您永远不会得到对不再存在的无效实例的引用。

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

下面的示例与上面的Person和Apartment示例相同,但有一个重要的区别。这一次,公寓类型的租户属性被声明为弱引用:

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

var john: Person?
var unit4A: Apartment?

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

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

下面是您将这两个实例链接在一起后的引用的样子:


Person实例仍然有对公寓实例的强引用,但是公寓实例现在有了对Person实例的弱引用。这意味着,当您将john变量的强引用设置为nil来打破它时,就不再有对Person实例的强引用:

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

因为不再有对Person实例的强引用,它被释放,租户属性被设置为nil:


对公寓实例惟一剩下的强引用来自unit4A变量。如果你打破了强引用,就没有对公寓实例的强引用了:

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

因为没有对公寓实例的更强引用,所以它也被释放:


weakReference03_2x.png

Unowned References 无主引用

与弱引用一样,无主引用也不会对它引用的实例保持强持有。
但是,与弱引用不同的是,当其他实例具有相同的生存期或更长的生存期时,将使用无主引用。通过在属性或变量声明前放置unowned关键字,可以表明这是一个unowned引用。

一个无主引用应该总是有一个值。因此,ARC从不将unowned引用的值设置为nil,这意味着unowned引用是使用非可选类型定义的。

重点:

下面的示例定义了两个类,Customer和CreditCard,它们为银行客户和该客户的可能信用卡建模。这两个类各自存储另一个类的一个实例作为属性。这种关系有可能创建一个强引用循环。

客户和信用卡之间的关系与上面弱引用例子中公寓和人之间的关系略有不同。在这个数据模型中,客户可能有信用卡,也可能没有,但是信用卡总是与客户相关联。CreditCard实例永远不会比它所引用的客户更长寿。为了表示这一点,Customer类有一个可选的card属性,但是CreditCard类有一个无主(且非可选)的Customer属性。

此外,新的CreditCard实例只能通过将数字值和customer实例传递给自定义CreditCard初始化器来创建。这确保在创建CreditCard实例时,CreditCard实例始终具有与之关联的customer实例。

因为信用卡总是有客户,所以您将其客户属性定义为一个无主引用,以避免强引用循环:

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

CreditCard类的number属性使用UInt64而不是Int类型定义,以确保number属性的容量足够在32位和64位系统上存储16位卡号。

下一个代码片段定义了一个可选的客户变量john,该变量将用于存储对特定客户的引用。这个变量的初值为nil,因为它是可选的:

var john: 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实例的无主引用。

由于无主客户引用,当您打破john变量所持有的强引用时,将不再有对客户实例的强引用:


unownedReference02_2x.png

因为没有对Customer实例的更强引用,所以它被释放了。在此之后,就不再有对CreditCard实例的强引用,它也被释放:

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

上面最后的代码片段显示,Customer实例和CreditCard实例的反初始化器都在将john变量设置为nil后打印它们的“反初始化”消息。

上面的示例展示了如何使用安全的无主引用。
Swift还为需要禁用运行时安全检查(例如出于性能原因)的情况提供了不安全的无主引用。与所有不安全的操作一样,您将负责检查代码的安全性。
通过编写unowned(不安全)来指示一个不安全的unowned引用。如果您试图在它引用的实例被释放后访问一个不安全的无主引用,那么您的程序将尝试访问实例曾经所在的内存位置,这是一个不安全的操作。

Unowned References and Implicitly Unwrapped Optional Properties 无主引用和隐式展开的可选属性

上面关于弱引用和无主引用的示例涵盖了两种更常见的场景,其中需要打破强引用循环。

Person和Apartment的例子显示了这样一种情况,两个属性都被允许为nil,有可能导致强引用循环。此场景最好使用弱引用来解决。

Customer和CreditCard示例显示了一种情况,其中一个属性允许为nil,而另一个属性不能为nil,这两种属性都有可能导致强引用循环。此场景最好使用无主引用来解决。

然而,还有第三种情况,在这种情况下,两个属性都应该始终有一个值,并且一旦初始化完成,任何一个属性都不应该为nil。在这个场景中,将一个类上的无主属性与另一个类上的隐式展开的可选属性相结合是很有用的。

这使得初始化完成后可以直接访问这两个属性(没有可选的展开),同时仍然避免了引用循环。本节将向您展示如何建立这样的关系。

下面的示例定义了两个类,Country和City,每个类都将另一个类的实例存储为属性。在这个数据模型中,每个国家必须始终有一个首都城市,并且每个城市必须始终属于一个国家。为了表示这一点,Country class有一个capital - City property,而City class有一个Country property:

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

要设置这两个类之间的相互依赖关系,City的初始化器接受一个Country实例,并将该实例存储在其Country属性中。

City的初始化器从Country的初始化器中调用。但是,Country的初始化器不能将self传递给City初始化器,直到一个新的Country实例被完全初始化,如两阶段初始化中所述。

为了满足这一要求,您可以将Country的capitalCity属性声明为一个隐式展开的可选属性,由其类型注释(City!)末尾的感叹号表示。这意味着capitalCity属性的默认值为nil,与任何其他可选属性一样,但是不需要像隐式展开Optionals中描述的那样展开它的值就可以访问它。

因为capitalCity有一个默认的空值,所以只要Country实例在其初始化器中设置了name属性,就会认为新Country实例已经完全初始化。这意味着国家参考和通过隐式初始化器可以开始自我财产一旦该国名称属性设置。因此初始化器可以将自我作为一个参数传递给城市时初始化国家初始化设置自己的是甚麽的财产。

所有这些都意味着您可以在一个语句中创建Country和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"

在上面的示例中,使用隐式展开的可选方法意味着满足所有两阶段的类初始化器需求。一旦初始化完成,就可以像非可选值一样使用和访问capitalCity属性,同时仍然可以避免强引用循环。

Strong Reference Cycles for Closures 闭包的强引用循环

您在上面已经看到,当两个类实例属性彼此持有一个强引用时,如何创建一个强引用循环。您还了解了如何使用弱引用和无主引用来打破这些强引用循环。

如果您将闭包分配给类实例的属性,并且该闭包的主体捕获该实例,则还可能发生强引用循环。可能会发生这种捕获,因为闭包的主体访问实例的属性,比如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属性,该属性表示元素的名称,如标题元素的“h1”、段落元素的“p”或换行元素的“br”。HTMLElement还定义了一个可选的文本属性,您可以将其设置为表示要在该HTML元素中呈现的文本的字符串。

除了这两个简单属性之外,HTMLElement类还定义了一个名为asHTML的惰性属性。此属性引用将名称和文本组合成HTML字符串片段的闭包。asHTML属性的类型是 () -> String,或者“一个不接受参数并返回字符串值的函数”。

默认情况下,asHTML属性被分配一个闭包,该闭包返回HTML标记的字符串表示形式。如果存在可选文本值,则此标记包含该值;如果不存在文本,则不包含文本内容。对于段落元素,闭包将返回"<p>some text</p>" 或 "<p />",这取决于text属性是否等于“some text”或nil。

asHTML属性的命名和使用有点像一个实例方法。但是,由于asHTML是一个闭包属性而不是实例方法,所以如果您想更改特定HTML元素的HTML呈现,可以使用自定义闭包替换asHTML属性的默认值。

例如,如果文本属性为nil,可以将asHTML属性设置为默认为某些文本的闭包,以防止表示返回空的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>"

注意:asHTML属性被声明为惰性属性,因为只有当元素实际需要作为某个HTML输出目标的字符串值呈现时才需要它。asHTML是一个惰性属性,这意味着您可以在缺省闭包中引用self,因为在初始化完成且self已知存在之前,惰性属性不会被访问。

HTMLElement类提供了一个单独的初始化器,它接受一个name参数和一个text参数(如果需要的话)来初始化一个新元素。该类还定义了一个反初始化器,它打印一条消息来显示HTMLElement实例何时被释放。

下面是如何使用HTMLElement类创建和打印一个新实例:

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

注意:上面的段落变量被定义为一个可选的HTMLElement,因此可以将它设置为nil,以演示强引用循环的存在。

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


closureReferenceCycle01_2x.png

实例的asHTML属性拥有对其闭包的强引用。但是,因为闭包在它的主体中引用self(作为引用self.name和self.text的一种方式),所以闭包捕获self,这意味着它持有对HTMLElement实例的强引用。在两者之间创建了一个强引用循环。(有关在闭包中捕获值的更多信息,请参见捕获值。)

尽管闭包多次引用self,但它只捕获对HTMLElement实例的一个强引用。

如果将变量paragraph设置为nil,并打破它对HTMLElement实例的强引用,由于强引用循环,HTMLElement实例和它的闭包都不会被释放:

paragraph = nil

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

Resolving Strong Reference Cycles for Closures 解决闭包的强引用循环

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

注意:Swift要求你写self.someProperty或self.someMethod()(而不仅仅是someProperty或someMethod()),当您在闭包中引用self的成员时。这有助于你记住,偶然捕捉self是有可能的。

Defining a Capture List 定义捕获列表

捕获列表中的每一项都是weak关键字或unowned关键字与对类实例(如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 and Unowned References 若引用和无主引用

将闭包中的捕获定义为一个无主引用,当闭包和它捕获的实例总是相互引用,并且总是同时释放时。

相反,当捕获的引用可能在将来的某个时刻变为nil时,将捕获定义为弱引用。弱引用始终是可选的类型,当它们引用的实例被释放时,将自动变为nil。这使您能够检查它们是否存在于闭包的主体中。

注意:如果捕获的引用永远不会变为nil,则应该始终将其捕获为无主引用,而不是弱引用。

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是一个无主引用,并没有对它捕获的HTMLElement实例保持强控制。如果将段落变量的强引用设置为nil, HTMLElement实例将被释放,如下面的例子中从其反初始化器消息的打印中可以看到:

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

<<返回目录

上一篇下一篇

猜你喜欢

热点阅读