16.ARC
Swift 使用自动引用计数(ARC)机制来跟踪和管理应用程序的内存。通常情况下,Swift 内存管理机制会一直起作用,我们无须自己来考虑内存的管理。ARC 会在类的实例不再被使用时,自动释放其占用的内存。
引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递。
1.自动引用计数的工作机制
- 当创建一个类的新的实例的时候,ARC 会分配一块内存来储存该实例信息。内存中会包含实例的类型信息,以及这个实例所有相关的存储型属性的值。
- 当实例不再被使用时,ARC 释放实例所占用的内存,并让释放的内存能挪作他用。这确保了不再被使用的实例,不会一直占用内存空间。
- 为了确保使用中的实例不会被销毁,ARC 会跟踪和计算每一个实例正在被多少属性,常量和变量所引用。哪怕实例的引用数为1,ARC都不会销毁这个实例。
- 将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称之为“强”引用,是因为它会将实例牢牢地保持住,只要强引用还在,实例是不允许被销毁的。
2.自动引用计数实践
class Person { let name: String; init(name: String) { self.name = name; print("\(name) is being initialized"); } deinit { print("\(name) is being deinitialized"); } } //这里还没有创建 var reference1: Person?; var reference2: Person?; //创建实例 reference1 = Person(name: "GofLee"); //这里才会创建并打印:"GofLee is being initialized\n" reference2 = reference1; reference1 = nil; reference2 = nil; //这个时候才会执行析构过程并打印:"GofLee is being deinitialized\n"
3.类实例之间的循环强引用
- 如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,这就是所谓的循环强引用。
下面的例子演示了循环强引用是怎么产生的:
class Person { let name: String; var apartment: Apartment?; init(name: String) { self.name = name; } deinit { print("\(name) is being deinitialized"); } } class Apartment { let unit: String; var tenant: Person?; init(unit: String) { self.unit = unit; } deinit { print("Apartment \(unit) is being deinitialized"); } } var john: Person? = Person(name: "John Appleseed"); var unit4A: Apartment? = Apartment(unit: "4A"); john!.apartment = unit4A; unit4A!.tenant = john; john = nil; unit4A = nil;
从上面的代码可以看到,Person
实例拥有一个指向Apartment
实例的强引用,而Apartment
实例也拥有一个指向Person
实例的强引用。如下图所示:
当断开john
和unit4A
变量所持有的强引用时,引用计数并不会降为0
,实例也不会被 ARC 销毁。图解如下:
4.解决实例之间的循环强引用
Swift 提供了两种办法用来解决在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。
- 弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。
- 对于生命周期中会变为nil的实例使用弱引用。相反地,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。
- 和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型。
示例1:弱引用解决上面循环强引用的问题
class Person { let name: String; var apartment: Apartment?; //weak var apartment: Apartment?; init(name: String) { self.name = name; } deinit { print("\(name) is being deinitialized"); } } class Apartment { let unit: String; weak var tenant: Person?; //注意此处的weak init(unit: String) { self.unit = unit; } deinit { print("Apartment \(unit) is being deinitialized"); } } var john: Person? = Person(name: "John Appleseed"); var unit4A: Apartment? = Apartment(unit: "4A"); john!.apartment = unit4A; unit4A!.tenant = john; john = nil; //打印"John Appleseed is being deinitialized\n" unit4A = nil; //打印"Apartment 4A is being deinitialized\n"
从上面的代码可以看到,这里Apartment
的tenant
属性被声明为弱引用,如下图所示:
当调用john = nil时,由于没有指向
Person
实例的强引用,所以实例会被销毁,图解如下:
此时唯一剩下的指向Apartment
实例的强引用来自于变量unit4A
。如果断开这个强引用,再也没有指向Apartment
实例的强引用了:
示例2:无主引用
//示例业务模型说明:一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系,Customer类有一个可选类型的card属性,但是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"); } } var john: Customer?; john = Customer(name: "John"); john!.card = CreditCard(number: 1234, customer: john!); john = nil;
【小结】:
- 两个属性的值都允许为nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决(如上面的Person和Apartment)。
- 一个属性的值允许为nil,而另一个属性的值不允许为nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决(如上面的
Customer
和CreditCard
)。 - 两个属性都必须有值,并且初始化完成后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。示例如下:
//示例业务模型说明:每个国家必须有城市,每个城市必须属于一个国家。为了实现这种关系,Country类拥有一个capitalCity属性,而City类有一个country属性 class Country { let name: String; var capitalCity: City!; //隐式解析可选类型,默认值为nil 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; } } var country = Country(name: "China", capitalName: "Beijing"); print("\(country.name)'s capital city is called \(country.capitalCity.name)");
5.闭包引起的循环强引用
先看一个示例:
class HTMLElement { let name: String; //HTML元素名称 let text: String?; //HTML元素显示的文本 lazy var asHTML: Void -> 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"); } } var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world"); print(paragraph!.asHTML()); paragraph = nil;
当调用paragraph = nil时,我们可以看到HTMLElement的deinit方法并没有调用。说明paragraph没有释放,现在我们用图来分析一下paragraph和默认闭包之间的引用关系:
6.解决闭包引起的循环强引用
在定义闭包时同时定义捕获列表作为闭包的一部分,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。
- 在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为
无主引用
。 - 相反的,在被捕获的引用可能会变为
nil
时,将闭包内的捕获定义为弱引用
。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil
。这使我们可以在闭包体内检查它们是否存在。
示例:解决上面闭包的循环强引用
class HTMLElement { let name: String; let text: String?; lazy var asHTML: Void -> String = { [unowned self] (void) -> String 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"); } }