- Swift 使用自动引用计数(ARC)机制来追踪和管理你的 App 的内存
- 当这些实例不在需要时,ARC会自动释放类实例所占用的内存。
引用计数只应用于类的实例。结构体和枚举是值类型,不是引用类型,没有通过引用存储和传递。
自动引用计数的工作机制
- 创建一个类的实例,ARC 分配一大块内存来存储这个实例的信息
- 类型信息
- 存储属性值的信息
- 当实例不需要时,ARC 会释放该实例所占用的内存
- 如果 ARC 释放了正在使用的实例内存,那么它将不会访问实例的属性,或者调用实例的方法
- 如果你试图访问该实例,你的app很可能会崩溃
- ARC 会跟踪和计算当前实例被多少属性,常量和变量所引用。
- 只要存在对该类实例的引用,ARC 将不会释放该实例。
- 无论你将实例分配给属性,常量或变量,它们都会创建该实例的强引用
- 称之为“强”引用,是因它将实例保持住,只要强引用在,实例是不允许被销毁的
自动引用计数实践
场景:实例内存的分配和释放操作
展示了自动引用计数的工作机制
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
- 由于可选类型的变量会被自动初始化为一个 nil 值,目前还不会引用到 Person 类的实例。
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John Appleseed")
// prints "John Appleseed is being initialized"
现在就有了一个从reference1 到该实例的强引用
实例又会多出两个强引用:
reference2 = reference1
reference3 = reference1
- 给其中两个变量赋值 nil 的方式断开两个强引用
reference1 = nil
reference2 = nil
reference3 = nil
// prints "John Appleseed is being deinitialized"
类实例之间的循环强引用
场景:某个类永远不会变成零强引用 = 永远不会销毁
循环引用:两个类实例,彼此持有对方的强引用
-
循环引用解决:
- 定义类之间的关系为弱引用( weak )或无主引用(unowned )来代替强引用
循环引用例子:
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") }
}
- apartment 属性是可选项,因为一个人并不总是拥有公寓
- tenant 属性是可选的,因为一栋公寓并不总是有居民
- 反初始化时输出信息,Person 和 Apartment 的实例是否像预期的那样被释放
- 两个变量都被初始化为 nil ,这正是可选项的优点:
var john: Person?
var unit4A: Apartment?
- 创建特定的 Person 和 Apartment 实例并将其赋值给 john 和unit4A 变量
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
- John 变量对Person 实例有一个强引用, unit4A 变量对 Apartment 实例有一个强引用
- 感叹号( ! )是用来展开和访问可选变量 john 和 unit4A 里的实例的
john!.apartment = unit4A
unit4A!.tenant = john
- 两个实例联系在一起之后,强引用的关系如图
Person 实例现在有了一个指向Apartment 实例的强引用
Apartment 实例也有了一个指向 Person 实例的强引用
断开 john 和 unit4A 变量持有的强引用,引用计数并不会降零,实例也不会被 ARC 释放
john = nil
unit4A = nil
没有任何一个反初始化器被调用
-
强引用关系如下图:
解决实例之间的循环强引用
- 对于生命周期中会变为 nil 的实例使用弱引用
- 对于初始化赋值后再也不会被赋值为 nil 的实例,使用无主引用
- 上面的 Apartment 例子中,在它的声明周期中,有时是”没有居民”的/可选的,因此适合使用弱引用来解决循环强引用。
弱引用
- 不会对引用实例强引用,不会阻止 ARC 释放
- 语法:声明属性或者变量时,在前面加上 weak 关键字
- 置 nil 操作:ARC 会在被弱引用的实例被释放,自动地设置弱引用为 nil (由于弱引用需要允许它们的值为 nil ,它们一定得是可选类型)
- 检查弱引用的值是否存在,就像其他可选项的值一样,并且你将永远不会遇到“野指针”
在 ARC 给弱引用设置 nil 时不会调用属性观察者。
- 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 实例是弱引用
- 当你断开 john 变量所保持的强引用时,再也没有指向 Person 实例的强引用
- 实例会被释放:
john = nil
// prints "John Appleseed is being deinitialized"
- Person 实例对 Apartment 实例强引用, Apartment 实例对Person 实例是弱引用
- 当你断开 john 变量所保持的强引用时,再也没有指向 Person 实例的强引用
- 实例会被释放:
john = nil
// prints "John Appleseed is being deinitialized"
- 没有强引用到 Person 它被释放掉了,并且 tenant 属性被设置为 nil
unit4A = nil
// prints "Apartment 4A is being deinitialized"
- 没有指向 Apartment 实例的强引用,该实例也会被释放:
注意
在使用垃圾回收机制的系统中,由于没有强引用的对象会在内存有压力时触发垃圾回收而被释放,弱指针有时用来实现简单的缓存机制。总之,对于 ARC 来说,一旦最后的强引用被移除,值就会被释放,这样的话弱引用就不再适合这类用法了。
无主引用
- 使用场景:非可选类型
- 优点:不需要在使用它的时候将它展开
- 缺点:ARC 无法在实例被释放后将无主引用设为 nil(因非可选类型变量不允许赋值为 nil)
如果你试图在实例的被释放后访问无主引用,那么你将触发运行时错误。
Customer 和 CreditCard ,模拟了银行客户和客户的信用卡
-
一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户
- 新 CreditCard 实例只有通过 number 值和 customer 实例到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位的卡号
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
- 关联了两个实例后的图示关系:
- 断开 john 变量持有的强引用时,再也没有指向CreditCard 实例的强引用,该实例也随之被释放了
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"
无主可选引用
场景:(非可选)无主引用不能为 nil,无主可选引用可为 nil (但不会自动置 nil)
使用无主可选引用时,需保证引用合法对象或 nil
追踪学校特定部门提供的课程
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
Course 有两个无主引用,一个是到部门,另一个是下一门学生要上的课程
每一门课程都是某些部门的一部分,所以 department 不是可选的
课程并不包含推荐的后续课程, nextCourse 是可选的
let department = Department(name: "Horticulture")// 创建了一个部门以及它的三个课程
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
// 初级和中级课程都有一个建议的后续课程存放在它们的 nextCourse 属性中
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
- nextCourse 维护了一个无主可选引用,指向了学生在完成本课程后应该继续的课程
- unowned 不能自动置nil,所以还是需要保证 nextCourse 指向了一个没有被释放的课程
- 从 department.courses 删除课程时,同样要移除其他课程对这个课程的引用
上述可选值的类型是 Optional ,也就是 Swift 标准库中的枚举。
总之,可选项是值类型不能被标记为unowned 这个规则中的例外。
包裹了类的可选项并不使用引用计数,所以你不需要对可选项维持强引用。
无主引用和隐式解包可选值属性
- 循环引用-解决
- 弱引用来解决:两个属性的值都允许为 nil
- Person 和 Apartment
- 无主引用解决:一个属性的值允许为 nil ,而另一个属性的值不允许为 nil
- Customer 和 CreditCard
- 一个类用无主属性,另一个类用隐式展开的可选属性:
- 两个属性都必须有值,并且初始化完成后永远不会为 nil
- 弱引用来解决:两个属性的值都允许为 nil
- 两个类, Country 和 City
- 每个国家必须有首都,每个城市必须属于一个国家
class Country {
let name: String
var capitalCity: City! // 有一个默认值 nil,保证 Country 的实例完全初始化完后, Country 的初始化器才能把 self 传给 City 的初始化器
init(name: String, capitalName: String) {
self.name = name // 一旦 name 属性被赋值后, Country 的初始化器就能引用并传递隐式的 self
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
}
}
- 以上的意义:通过一条语句同时创建 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 属性初始化完成后,能像非可选项一样使用,同时还避免了循环强引用
闭包的循环强引用
闭包循环引用:比属性循环引用,多了变量捕获
场景:闭包赋值给实例属性,闭包又捕获这个实例
-
闭包捕获实例场景:
- 闭包函数体访问了实例某个属性,比如self.someProperty
- 闭包调用了一个实例的方法,例如self.someMethod()
闭包循环引用本质:闭包和类,都是引用类型
用一种简单的模型表示 HTML 中的一个单独的元素:
class HTMLElement {
let name: String // 元素的名称,如表示标题元素的 "h1" 、代表段落元素的 "p" 、或者代表换行元素的 "br"
let text: String?// 可选的属性 text ,它可以用来设置和展现 HTML 元素的文本
// 这个属性引用了一个将 name 和 text 组合成 HTML 字符串片段的闭包
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 是闭包,用自定义的闭包来取代默认值
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 输出的字符串时,才需要使用asHTML 。
实际上 asHTML 是延迟加载属性,意味在默认闭包可用 self ,因只有当初始化完成以及 self 确实存在后,才能访问延迟加载属性。
- 用 HTMLElement 类创建实例并打印消息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints"hello, world"
- HTMLElement 类产生了和 asHTML 默认值的闭包之间的循环强引用
- asHTML 属性持有闭包的强引用
- 闭包在其闭包体内使用了 self (引用了 self.name 和 self.text ),因此闭包捕获了 self ,这意味着闭包又反过来持有了 HTMLElement 实例的强引用
尽管闭包多次引用了 self ,它只捕获 HTMLElement 实例的一个强引用。
paragraph = nil
// 实例和它的闭包都不会被释放,也是因为循环强引用
- HTMLElement 的反初始化器中的消息并没有被打印,证明了 HTMLElement 实例并没有被销毁
解决闭包的循环强引用
- 场景:通过弱/无主引用,标记闭包捕获列表的变量,解决闭包和实例的循环引用
建议显式使用 self.someProperty 或者 self.someMethod (而不只是someProperty 或 someMethod ),有助于提醒捕获了 self ,避免循环引用
定义捕获列表( closuer 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
}
弱引用和无主引用
- 闭包和捕获的实例总是互相引用并总是同时释放时:
- 将闭包内的捕获定义为无主引用
- 被捕获的引用可能会变为 nil 时
- 定义一个弱引用的捕获
如果被捕获的引用永远不会变为 nil ,应该用无主引用而不是弱引用。
- 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 实例:
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// prints "<p>hello, world</p>"
- 使用捕获列表后引用关系
- 闭包以无主引用的形式捕获 self ,并不会持有 HTMLElement 实例的强引用
paragraph = nil
// prints "p is being deinitialized"