自动引用计数(Automatic Reference Counting)
Swift 使用 自动引用计数器(ARC)
去追踪和管理你应用内存的使用。大多数情况下,这意味着在Swift中内存管理“自动生效”,你不需要自己管理内存。ARC会在类对象不再使用的时候自动释放内存。
然而,在少数情况下 ARC在为你管理内存时 需要你提供代码间的关系等更多的信息。这一章阐述了这些情况并且向你展示了如何使用ARC管理你的应用所有内存。在Swift中使用ARC非常接近OC中的过渡到ARC
ARC如何工作(How ARC Works)
每当你创建一个类的新的对象,ARC分配一块内存空间去存储这个对象的信息。这块内存持有实例类型的信息,以及与该实例相关联的任何存储属性的值。
此外,当一个对象不再需要时,ARC会释放掉对象占用的内存用于其他用途。这就确保了当类实例不再使用时不会占据内存空间。
然而,如果如果ARC销毁了一个仍然有用的实例,就不能再访问该实例的属性以及调用该实例的方法。如果你尝试访问它,会使你的应用崩溃。
为了确保实例在它们仍会被使用的情况下不会被释放,ARC追踪当前每个实例被多少属性、常量、变量被引用。实例只要有一个有效的引用,ARC就不会释放这个实例
(ARC tracks how many properties, constants, and variables are currently referring to each class instance
)。
使这成为可能,每当你把类实例赋给属性、常量、或者变量时,需要强引用这个实例。之所以称为强引用是它真实持有了实例,并且只要这个强引用还在,就不允许实例被释放。
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?,不是Person),它们被自动初始化为nil,并且当前没有引用Person实例
var reference1:Person?
var reference2:Person?
var reference3:Person?
你现在可以创建一个Person实例并且把它赋给这三个变量中的一个。
reference1 = Person(name:"John Appleseed")
注意当调用Person的构造方法时打印了"John Appleseed is being initialized",说明初始化已经完成。
因为创建的Person实例赋值给了reference1变量,所以现在reference1强引用着Person实例。因为有一个强引用,ARC确保这个实例在内存中,不会被销毁。
如果你把Person实例赋值给更多的变量,就会建立更多的强应用:
reference2 = reference1
reference3 = reference1
现在有三个强引用引用着这个实例。
如果你打断其中的两个强引用(包括最先强引用)通过赋值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实例不在被引用时销毁掉。
然而,在编码的时候,有可能遇到一个类的实例永远不会有强引用为0的情况。这种情况可能发生在两个类实例彼此强引用对方,导致了互相持有。这被称作循环引用
。
你可以通过定义两个类的一些关系例如weak
或者unowned
引用代替强引用。这个过程描述在解决两个类实例间的循环引用。然而,在你学习如何解决循环引用之前,理解循环引用是如何引起的是很有用的。
这里有一个怎样偶然的引起循环引用的例子。这个例子定义了一个Person类和Apartment类
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment ?
deinit { print("\(name) is being deinitialzed") }
}
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实例有一个Stirng类型的unit属性和一个可选的被初始化为nil的tenant属性。这个tenant属性是可选的,因为一个公寓不一定总有租客。
两个类都定义了析构函数,打印了类的实例被销毁的信息。这可以确保你看到是否Person和Apartment像你期望的那样被销毁。
下面这个代码块定义了两个可选类型的变量分别叫做john和unit4A,下面设置了具体的Apartment和Person实例。两个变量都被初始化为nil。
var john: Person?
var unit4A: Apartment?
现在可以创建具体的Person实例和Apartment实例并且赋给john和unit4A变量:
john = Person(name: "John Appleaseed")
unit4A = Apartment(unit: "4A")
这里展示了强引用是如何创建和赋值这两个实例的。john变量现在强引用着Person实例,unit4A变量强引用着Apartment实例。
你现在可以把这两个实例联系在一起,让人拥有一套公寓,让公寓有一个租客。注意到感叹号被用于解绑并且获取存储在john和unit4A中的可选变量,便于给他们的属性设置值。
这里展示了如何将两个实例联系到一起。
遗憾的是,连接这两个实例制造了它们之间的循环引用。Person实例现在强引用着Apartment实例,并且Apartment实例强引用着Person实例,因此,当你打断了john和unit4A持有的强引用,引用计数没有归零,这些实例不会被ARC销毁。
john = nil
unit4A = nil
注意到当你设置这两个变量的值为nil时,两个类的实例都没有调用析构函数。循环引用阻止了Person实例和Apartment实例被销毁,在应用里引起了内存泄露。
这里展示了当你给这些变量设置nil时的强引用关系。
在Person实例和Apartment实例之间仍然保持着强引用,没有办法打断。
解决类实例间的循环强引用(Resolving Strong Reference Cycles Between Class Instances)
Swift提供两种方式去解决循环强引用,通过作用在类的属性上的关键字:weak
和 unowned
。
Weak 和 unowned引用能够使一个实例在循环引用中用不是强引用的方式引用另一个实例。之后实例互相引用时就不会造成循环强引用。
当引用的另一个实例有更短的生命周期时使用weak引用
--就是,另一个实例会先被销毁。在上面的Apartment例子里,在公寓的生命周期里,会存在一个时间点,没有租客。所以在这种情况下使用weak引用是一种合适的做法。相反的,使用unowned引用,当引用的实例的生命周期大于等于自己的生命周期
。
Weak 引用 (Weak References)
Weak引用不会对它持有的实例强引用,也不会阻止ARC释放它引用的实例。这种行为阻止了成为循环强引用。你通过在一个属性或者变量声明之前放置weak关键字来表示这是一个若引用
。
因为弱引用没有对他引用的实例strong hold,所以它引用的实例可以被销毁即使有弱引用的存在。因此,ARC自动设置一个弱引用为nil 当这个实例弱引用的实例被销毁的时候
。同时,因为弱引用的实例需要在运行时被ARC改为nil,他们总是被定义为可选类型的变量,而不是常量
。
你可以检查弱引用是否存在一个值,像其他的可选值一样,你将永远不会引用一个不存在的无效实例。
注意
当ARC设置弱引用为nil时,属性的观察者不会被调用(Property observers aren’t called when ARC sets a weak reference to nil.)(我理解的是不会走KVC的方法)
下面这个例子和上面的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: Stirng) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
两个变量之间的强引用(john 和 unit4A) 和两个实例之间的联系和原来创建的方式相同:
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
下图展示了这两个实例间的引用关系
Person实例仍然强引用Apartment实例,但是Apartment实例现在弱引用Perosn实例。这就意味着当你通过设置john变量的值为nil来打断john的强引用时,就没有其他的强引用来引用Person实例了。
john = nil
// Prints "John Appleseed is being deinitialized"
因为没有更多的强引用去引用Person实例,所以Person实例被销毁,同时将tenant属性被赋值为nil:
Apartment实例唯一剩下的强引用来源于unit4A变量,如果你打断这个强引用,就没有别的强引用去引用这个实例了
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
因为没有更多的强引用引用Apartment实例,它也被销毁了。
注意
在使用垃圾回收的系统中,weak指针有时被用于实现简单的缓存机制,因为没有被强引用的对象被销毁只有当内存达到峰值时才会触发垃圾回收。然而,在ARC中,当最后一个强引用被移除时会立即被销毁,所以使用weak不适合做这件事。
Unowned引用 (Unowned References)
像weak引用一样,unowned引用不会对它引用的实例strong hold。与weak引用不一样的,unowned引用被用于引用的实例的生命周期大于等于自身
。你通过在一个属性声明前放置关键字unowned来表示unowned引用。
一个unowned引用被认为一直有一个值,因此,ARC不会设置unowned引用的值为nil,这意味着unowned引用被定义为非可选类型。
关键
使用一个unowned引用,仅仅当你确定你引用的实例不会被释放。
如果你尝试访问一个被销毁的unowned引用的实例,你会触发运行时错误。
下面这个例子定义了两个类,Customer和CreditCard,模拟了银行的顾客和这个顾客可能拥有的引用卡。这两个类实例都存储了另一个类实例作为属性。这种关系有潜在的可能制造循环强引用。
Customer和CreditCard之间的关系与Apartment和Person之间的关系有轻微的不同。在这个情形中,一个顾客可能拥有或者不拥有一张信用卡,但是一张信用卡会一直关联一个顾客。一个信用卡实例不会脱离于它引用的顾客实例单独存活。表达这个关系,Customer类有一个可选的card属性,但是creditCard类有一个unowned(非可选)customer属性。
此外,一个新的CreditCard实例只能通过一个number和一个customer实例去构造,这一点也确保了当Creditcard实例创建时,一直拥有一个与它关联的customer实例。
因为一张信用卡会一直有一个顾客,所以定义他的customer属性使用unowned引用来避免循环强引用。
class Customer {
let name: Stirng
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属性的容量足够大去存储一个16位卡的数字,在32位或者64位系统中。
下面的代码块定义了一个可选的Customer变量john,将被用来存储一个具体的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实例有一个unowned引用,引用着Customer实例。
因为使用了unowned customer 引用,当你打断john的强引用,就没有额外的强引用引用着Customer实例了
因为没有更多的强引用引用着Customer实例,它被销毁了,随着它被销毁,也没有更多的强引用引用着CreditCard实例,所以它也会被销毁。
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上面的代码块展示了当john变量被设置为nil时,Customer实例和CreditCard实例都调用了对应的析构函数。都被销毁了。
注意
上面的例子展示了如何安全的使用unowned引用,Swift也提供了不安全的unowned引用为了你需要忽视runtime安全检查的情况--例如由于性能的原因。像所有不安全的操作一样,你需要自己保证代码的安全
你通过写unowned(unsafe)
来指明这是一个不安全的unowned引用。如果你尝试访问一个已经被释放的实例,你的项目将会尝试去访问一块曾经使用过的内存空间,这可能是一个不安全的操作。
Unowned引用 和 隐式释放可选属性(Unowned References and Implicitly Unwrapped Optional Properties)
上面的例子覆盖了很常见的两种情形去避免循环强引用。
Person和Apartment例子展示了两个属性都允许被赋值为nil的情形,这种情景最好使用weak去解决循环强引用问题。
Customer和CreditCard例子展示了其中一个属性可以被赋值为nil,另一个属性不能被赋值为nil的情形,这种场景最好使用unowned引用解决循环强引用问题。
然而,还有第三种情况,两种属性都一直拥有值,一旦它们被创建都不会被赋值为nil。在这个场景中,要在一个类的属性中使用unowned同时在另一个类中使用 implicitly unwrapped optional。
这样确保了两个属性一旦被创建就可以被直接访问(不用解绑),这样做仍然避免了循环引用问题。这一节向你展示了如何建立这样的关系。
下面的例子定义了两个类,Country 和 City,每个实例存储了另一个类实例作为自己的一个属性。在这个数据模型中,每个国家必须一直有一个首都城市,并且每个城市必须一直有一个所属的国家。为了表示这个关系,Counrty类有一个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
}
}
未完待续。。。