swift使用自动引用计数(ARC)来追踪和管理app的内存使用。大多数情况下,这意味着内存管理在swift中“just works”,你不需要关心管理。ARC自动释放被类实例使用的内存当这些实例不再需要的时候。
不过,少数情况下ARC需要更多的关于你的代码间的关系来为你管理内存。这一章节描述这些情况并且展示你如何使ARC管理你的APP的全部的内存。在swift中使用ARC和在Transitioning to ARC Release Notes中描述的为Objective-C使用ARC的描述的方式很相似。
引用计数只对类实例使用。结构体和枚举是值类型,不是引用类型,不通过引用存储和传递。
ARC如何工作(How ARC Works)
每当你创建一个类的实例的时候,ARC开辟一段内存来存储实例的信息。这个内存保存实例的类型的信息,和任何关联实例的存储属性的值。
除此之外,当实例不再需要的时候,ARC释放那个实例使用的内存所以内存代替用于其他的用途。这确保当他们不需要的时候类实例不会在内存中占空间。
不过,如果ARC要释放一个仍然在用的实例,不能再访问实力的属性,或者调用实例的方法。实际上,你过你尝试访问实例,你的APP大多数会崩溃。
要确保当他们仍然使用的时候实例没有消失,ARC追踪有多少属性,常量,变量现在引用每个类的实例。只要至少有一个引用实例的的引用仍然存在ARC不会释放实例。
要使其成为可能,任何时候你把实例分配给一个属性,常量,或者变量,那个属性,常量,或者变量是对实例强引用的。引用成为强引用是因为它保持一个稳固的持有那个实例,只要强引用在不允许它被释放。
使用中的ARC(ARC in Action)
这是一个自动引用计数如何工作的例子。这个例子开始有一个名为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?,不是Person),他们自动用nil初始化,现在没有引用一个Person实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?你现在可以创建一个新的Person实例并且把它分配给三个变量中的一个:
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"注意在你调用person类的初始化器的时候打印了“John Appleseed is being Initialized”。者确定了初始化发生了。
因为新的Person实例已经分配给了reference1变量,现在从reference1中有一个对新的Person实例的强引用。因为有至少一个强引用,ARC确定这个Person保存在内存中并且不会释放。
如果你分配相同的Person实例给多余两个的变量,对那个实例有多于两个的强引用建立:
reference2 = reference1
reference3 = reference1现在对单个Person实例有三个强引用。
如果你分配给两个变量nil来打断两个强引用(包括原始的引用),剩余一个强引用,Person实例没有释放:
reference1 = nil
reference2 = nilARC知道第三个和最后一个强引用中断不会释放Person实例,这个时候很明确你不再需要使用Person实例:
reference3 = nil
// Prints "John Appleseed is being deinitialized"类实例之间的强引用循环(Strong Reference Cycles Between Class Instances)
上面的例子中,ARC可以追踪你创建的新的Person实例的引用数量并且当你不再需要他的时候销毁Person实例。
不过,代码会有可能有一个类的实例都不会有零个引用的时候。如果两个类的时候互相拥有强引用的时候会发生,导致每个实例使其他实例仍然存在。这就是强引用循环。
通过把类之间的关系定义为弱的或者非拥有的引用代替强引用来解决引用循环。这个过程在Resolving Strong Reference Cycles Between Class Instances中描述。不过,在你学会如何解决强引用循环之前,了解如何产生这样的循环对你有帮助。
这是一个强引用循环如何意外创建的例子。这个例子定义了两个类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属性。apartment属性是可选的,因为一个人不是全都有一个公寓。
相似的,每个apartment实例有一个string类型的unit属性和一个初始化为nil的tenant属性。tenant属性是可选的,因为一个公寓可能不总是有住客。
这些类也定义了一个deinitializer,打印那个类的实例被析构的事实。使你可以看到Person和apartment的实例是否按期望的那样被析构。
这面的代码定义了两个名为john和unit4A的可选类型的两个变量,将会设置为一个特殊的apartment和Person实例。两个变量有初始化值nil,由于是可选类型的:
var john: Person?
var unit4A: Apartment?现在你可以创建Person和apartment实例并且把这些新的实例分配给john和unit4A变量:
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")这里是在创建和分配两个实例后强引用看起来的样子。john变量现在有对新的Person实例的强引用:
你现在可以把两个实例链接在一起所以person有一个apartment,并且apartment有一个teant。注意用了感叹号(!)来解包并方位存储在john和unit4A中的可选变量的实例,所以这些实例的属性可以设置为:
john!.apartment = unit4A
unit4A!.tenant = john这是在你链接两个实例之后强引用看起来的样子:
不幸的是,链接这两个实例在他们之间创建了强引用。person实例现在有对apartment实例的强引用,并且apartment实例有一个对person实例的强引用。所以,当你打破john和unit4A变量持有的强引用的时候,引用计数没有减到0,实例没有被ARC释放:
john = nil
unit4A = nil注意当你设置这两个变量为nil的时候两个deinitializer都没有被调用。强引用循环阻止了person和apartment实例被释放,在你的APP中引起了内存泄露。
这是在你把john和unit4A设置为nil之后强引用看起来的样子:
person实例和apartment实例之间的强引用保留并且没有打破。
解决类实例之间的强引用循环(Resolving Strong Reference Cycles Between Class Instances)
当你使用类的属性时swift提供了两种方式来解决强引用循环:weak 引用和非持有 引用。
弱和非持有引用使引用循环中的一个实例不用保持强持有它来引用另一个实例。实例可以不用创建一个强引用循环来互相引用。
当其他实例有一个短暂的生命期的时候使用一个弱引用--也就是,当其他实例可以先被释放的时候。在上面的apartment例子中,apartment在它的声明其中适合没有住客,所以weak引用适合打破这种情况的引用。相对的,当其他实例有一样的声明周期或者更长的时候使用非持有引用。
弱引用(Weak References)
若引用是一种不对它引用的实例保持强持有的引用,所以不会阻止ARC处理强引用循环。这种表现防止了引用变成强引用循环的一部分。通过在属性或者变量声明之前放一个weak关键字来指定一个弱引用。
因为弱引用不对他引用的实例保持强持有,当弱引用仍然引用它的时候那个实例有可能被释放。所以ARC在他引用的实例被释放的时候自动将弱引用设置为nil。所以,因为弱引用需要允许它的值在运行时改为nil,他们通常声明为可选类型的变量而不是常量。
你可以查看在弱引用中一个值的存在,像其他可选值一样,你不能用引用来结束已经不存在的实例。
当ARC把弱引用设置为nil的时候属性观察这不会被调用。
下面的例子和上面例子的person和apartment例子一样,有一个重要的区别。这一次,apartment类型的tenant属性声明为了弱引用:
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实例仍然对apartment实例有强引用,但是apartment实例对person实例有一个弱引用。这意味着当你通过把它设置为nil来打破john变量引用的强引用的时候,对person实例没有强引用了:
john = nil
// Prints "John Appleseed is being deinitialized"因为对person实例没有强引用,他被释放并且tenant属性设置为nil:
对apartment实例的唯一留下的强引用是来自unit4A变量的。如果你打破那个强引用,对apartment实例没有别的强引用了:
unit4A = nil
// Prints "Apartment 4A is being deinitialized"因为对apartment实例没有更多的强引用,他也会被释放:
在使用垃圾回收的系统中,弱指针有时候用来实现一个单一的缓冲机制,因为只有当内存强制触发垃圾回收的时候没有强引用的引用被释放。不过,有ARC,只要他们最后的强引用被移除了值就会被释放,对于这个目的使用弱引用不合适。
非拥有引用(Unownen References)
像一个弱引用,一个非持有的引用对它引用的实例没有保持强持有。不像弱引用,不过,非持有引用在其他实例有相同的或者更长的生命周期的时候使用。通过在属性或者变量声明之前放置一个unowed关键字来指定一个非指定引用。
一个非持有的引用期望一直有值。结果是,arc从不把非持有引用设置为nil,意味着非持有引用使用非可选类型定义。
只有在你确定引用会指向一个没有被释放的实例的时候使用。
如果你尝试在实力已经被释放之后访问非持有引用的值,你会得到一个运行时错误。
下面的例子定义了两个类,customer和creditcard,模型了一个银行顾客和一个那个顾客的信用。这两个类都把另外一个类的实例存储为了一个属性。这个关系有创建一个强引用循环的潜在可能。
在customer和creditcard之间的关系和在上面的弱引用例子中看到的apartment和person之间的关系有一点不同。在这个数据模型中,一个顾客可能有也可能没有一个信用卡,但是一个信用卡一直会关联着一个顾客。一个creditCard实例从不会比它引用的customer存在的久。为了表示这一点,customer类有一个可选的card属性,但是cerditCard类有一个非持有(非可选)的customer属性。
此外,只能通过给一个自定义creditcard初始化器传递一个number值和一个customer实例来创建一个新的creditcard实例。这确保了creditcard实例当它被创建的时候一直有一个customer实例关联着它。
因为一个信用卡一直有一个customer,把他的customer属性定义为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") }
}creditcard类的number属性用一个UInt64类型而不是Int类型定义,来确保number属性的容量在32位和64位的系统中足够存储16位卡号
下面的代码定义了一个可选的名为john的customer变量,将用于存储一个特殊customer的引用。这个变量有一个nil的初始化值,由于是可选类型的:
var john: Customer?你现在可以创建一个customer实例,并用它来初始化和分配一个新的creditCard实例作为customer的card属性:
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)这是引用的情况,现在你链接了两个实例:
customer实例现在有一个对creditCard实例的强引用,creditCard实例对customer实例有一个非持有的引用。
因为非持有customer引用,当你打破john变量持有的强引用的时候,对customer实例没有更多的强引用:
因为对customer实例没有更多的强引用,他被释放了。在这发生之后,对creditCard实例没有更多的强引用,他也被释放了:
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"上面最后的代码段展示了customer实例和creditcard实例的析构器在john变量设置为nil之后打印他们的析构信息。
上面的例子展示了如何使用安全非持有引用。swift也为但你需要禁用运行时安全检查的时候提供了不安全非拥有引用--例如,为了性能因素。像其他非安全操作一样,你负责检查代码的安全。通过写unowned(unsafe)指定一个非安全的非拥有引用。如果你尝试在它引用的实例销毁之后访问一个不安全的非拥有引用,你的程序会尝试访问实例之前用的内存地址,是不安全的操作。
非拥有引用和隐式解包可选属性(Unowned References and Implicitly Unwrapped Optional Properties)
上面关于weak和unowned引用的例子用了两个非常普通的需要打破强引用循环的场景。
person和apartment例子展示了一种情况,两个属性都允许是nil,有潜在引起强引用循环的可能。这种情况最好使用weak引用解决。
customer和creditcard的例子展示了一种情况,一个允许是nil的属性和一个不允许是nil的属性有导致强引用循环的风险。这种情况最好用unowned引用解决。
不过,有第三个情况,两个属性都有值,一旦初始化完成两个属性都不会是nil。这种情况,把一个类中的非拥有属性和另一个类中的隐式解包可选类型属性结合起来比较有用。
这使两个属性可以在一旦初始化完成的时候直接被访问(不需要可选解包),同时也避免了引用循环。这一节向你展示了如何设置这种关系。
下面的例子定义了两个类,Country和City,每个把另一个类的实例存储为一个属性。在这个数据模型中,每个国家必有一个首都,每个城市必属于一个国家。要表示这个,Country类有一个capitalCity属性,City类有一个Country属性:
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的初始化器知道一个新的Country实例完全初始化之前不会把自己传给City初始化器,像Two-Phase Initialization中描述的。
要处理这个要求,将Country的capitalCity属性声明为一个隐式解包的可选属性,在它的类型声明后标记感叹号来指明(City!)。这意味着capitalCity属性有一个nil的默认值,想起他可选类型一样,但是可以像在Implicitly Unwrapped Optionals描述的不用解包它的值来访问。
因为capitalCity有一个默认的nil值,一旦在它的初始化器中Country实例设置了它的name属性就当做一个新的Country实例完全初始化完成了。这意味着一旦设置了name属性Country初始化器可以开始引用和传递隐式的self属性。Country初始化器可以在Country初始化器设置他自己的capitalCity属性的时候把self作为City初始化的一个参数。
全部的这些意味着你可以在一行语句中创建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)
上面看到了当两个类实例属性只有相互的强引用的时候如何产生一个强引用循环。也看到了如何用weak和unowned引用来打破这些强引用循环。
如果你给一个类的实例的属性分配一个闭包也可能引发一个强引用,这个闭包的主体捕获实例。这个捕获可能引发因为闭包的主体访问实例的属性,例如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也定义了一个可选的text属性,你可以把它设置为一个String来表示在HTML元素中渲染的文本。
除了这些简单的属性,HTMLElement类定义了一个名为asHTML的懒加载属性。这个属性引用了一个将name和text合并到HTML字符串块中的闭包。asHTML属性是()->String类型的,或者“一个不接受参数并且返回一个String值的函数”。
默认的情况,给asHTML分配一个返回一个表示HTML标签的字符串的闭包。这个标签包含可选的text值(如果它存在的话),如果text不存在的话没有文字内容。对于段落元素,闭包将返回“<p>some text</p>”或者"<p/>",取决于text属性是否等于“some text”或者“nil”。
asHTML属性的命名和使用和实例方法相似。不过,因为asHTML是一个闭包属性而不是实例方法,你可以用一个自定义闭包代替asHTML属性的默认值,如果你想改变特定的HTML元素的HTML渲染。
例如,如果text属性是nil,asHTML属性可以设置为一个默认是text的闭包,为了防止表示返回一个空的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>"上面的paragraph变量定义为一个可选的HTMLElement,所以在下面他可以设置为nil来演示强引用循环的产生。
不幸的是,HTMLElement类,上面写的,在HTMLElement实例和用作它的默认asHTML值的闭包之间创建了强引用,这是循环的样子:
实例的asHTML属性持有一个对它的闭包的强引用。不过,因为闭包在它的主体中引用了self(引用self.name和self.text的方式),闭包捕捉了self,意味着他对HTMLElement实例持有一个强引用。在两者之间产生了一个强引用循环。(更多关于捕捉值的信息,查看Capturing Values.)。
即使闭包引用了self多次,他只对HTMLElement实例保持了一个强引用。
如果你把paragraph设置为nil并打破他对HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被释放,因为强引用循环:
paragraph = nilHTMLElement析构器中的信息也没有打印,说明HTMLElement实例没有被释放。
解决闭包的强引用循环(Resolve Strong Reference Cycles for Closures)
通过在闭包定义中定义一个捕捉列表来解决闭包和类实例之间的强引用循环。捕捉列表定义了在闭包体中捕捉一个或者多个引用类型的规则。和两个类实例之间的强引用一样,把每个捕捉引用声明为一个weak或者非持有引用而不是强引用。weak或者unowned的合适的选择依靠你代码不同部分之间的关系。
swift需要你在闭包中引用self的成员的时候写self.someProperty或者self.someMethod()(而不是someProperty或者someMethod())。这帮助你记住这可能会意外捕捉self。
定义捕捉列表(Defining a Capture List)
在捕捉列表中的每个对象是一对weak或者unowned关键字和对一个用某个值初始化的类实例(例如self)或者变量的引用(例如delegate = self.delegate)。这些对写在一对方括号中,用逗号分隔。
如果提供了就在闭包参数列表和返回类型之前放置捕捉列表:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}如果因为他们可以从上下文中推导出来name闭包不用指定参数列表或者返回类型,在每个闭包开始的地方放置捕捉列表,后面跟着in关键字:
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}弱引用和非拥有引用(Weak and Unowned References)
当闭包和他的捕捉的实例会一直互相引用的时候把闭包中的捕捉定义为一个unowned的引用,并且会在同一时间被释放。
相反的,当捕捉的引用可能在未来某个时间点变成nil的时候把捕捉定义为weak引用。weak引用通常是可选类型,当他们引用的实例被释放的时候自动变为nil。这样你可以在闭包体中检查他们的存在。
如果捕捉引用不会变成nil,它应该被作为unowned引用被捕捉,而不是weak引用。
一个非拥有的引用是合适的方法来解决上面Strong Reference Cycles for Closures中的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")
}
}HTMLElement的实现和之前的实现完全一样,除了在asHTML闭包中的捕捉列表。这种情况下,捕捉列表是[unowned self],意思是“将self作为非持有引用而不是强引用”。
你可以像之前一样创建和打印HTMLElement实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"这是用捕捉列表代替后引用的样子:
这一次,闭包对self的捕捉是一个unowned引用,没有保持对他捕捉的HTMLElement实例的强引用。如果你把paragraph变量中的强引用设置为nil,HTMLElement实例会被释放,像下面例子中它的打印的析构器信息看到的一样:
paragraph = nil
// Prints "p is being deinitialized"更多关于捕捉列表的信息,查看Capture Lists。