ARC简介
ARC负责跟踪和管理应用程序的内存状态,一般情况下不需要考虑内存管理问题,当一个实例不再需要时,ARC会自动释放该实例占用的内存,ARC仅仅适用于类的实例,例如结构体,枚举是值类型,它们不通过引用传递和存储,所以ARC不会管理他们。
ARC是如何工作的?
每次创建一个类的实例的时候,内存会开辟一块空间去存放该实例以及它的存储属性的值,当ARC将该实例释放掉之后,就无法再去调用该实例的属性或者函数,如果坚持调用的话,则可能会出现崩溃的问题。
为了确保实例仍然在需要时保持存活,ARC会跟踪当前引用的实例的每个属性,只要至少有一个对该实例有强引用,ARC就不会释放该实例。
举例说明
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) 初始化")
}
deinit {
print("\(name) 被释放")
}
}
let person1: Person?
let person2: Person?
let person3: Person?
我们定义了三个Person,但是Person都是可选(optional)类型,所以它们在内存中并不存在,他们的值也是nil,并且也不会引用Person实例
person1 = Person(name: "Hellolad") // Hellolad 被初始化
当我们把Person(name: "Hellolad")
赋值给person1后,person1不再为nil,person1这时对Person有了一个强引用,这时ARC就起作用了,它跟踪到了该实例,并且保证该实例不会被释放
person2 = person1
person3 = person1
当我们把person1
赋值给了person2
person3
,这两个实例没有被初始化,只是它们强引用了和person1
同样的person实例
person1 = nil
person2 = nil
当我们把这个两个实例置为nil之后,deinit
函数并不会执行,因为ARC知道还有person3
在引用着person1,所以该实例仍然不会被释放
person3 = nil // Hellolad 被释放
当我们把person3
也置为nil的时候,person1
才能被释放掉,并打印 Hellolad 被释放
循环引用
当两个类互相引用彼此的时候,就会造成循环引用。
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) 被释放")
}
}
class Apartment {
let unit: String
init(unit: String) {
self.unit = unit
}
var tenant: Person?
deinit {
print("Apartment \(unit) 被释放")
}
}
上面的代码我们故意创建一个可以造成循环引用的例子,每一个Person都可以拥有一个公寓,每个公寓也都可以拥有一个人,他们的引用关系如下图:
图片: The Swift Programming Language AutomaticReferenceCounting
var hellolad: Person?
var loveApartment: Apartment?
hellolad = Person(name: "Hellolad")
loveApartment = Apartment(unit: "502")
我们创建了一个人和爱情公寓的两个实例,他们现在都强引用着各自的实例,但并没有强引用对方,因为我们还有给他们的apartment
和tenant
赋值。
hellolad?.apartment = loveApartment
loveApartment?.tenant = hellolad
然后我们把他们的各自的实例,赋值对应到他们各自的实例的属性apartment
和tenant
,此时他们的引用关系如下图:
图片: The Swift Programming Language AutomaticReferenceCounting
hellolad = nil
loveApartment = nil
当我们把两个实例置为nil的时候,ARC会把他们的引用计数置为0,这是没有错的,但是控制台却没有打印任何释放实例的信息?
此时我们再看他们的引用情况:
图片: The Swift Programming Language AutomaticReferenceCounting
我们发现他们自已已经移除了对Person
和Apartment
的互相引用,所以ARC依然在追踪他们的属性的引用计数.
解决循环引用的两种方案
Swift在类实例的属性相互引用造成的循环引用中,提供了两种解决方案, 弱引用(weak
)和无主引用(unowned
),weak
和unowned
可以使一个实例能和另一个实例保持互相引用,但却不是强引用。两者的区别weak
在OC里和weak
是一样的,unowned
和OC里的unsafe_unretained
。unowned
设置以后它的原理的引用如果被释放了,它依然会保持着一个对被释放的的实例的引用,而weak
就不一样了,如果它引用的对象被置为nil那么它也会自动的指向nil(所以被weak的对象需要是Optional)
weak var apartment: Apartment?
weak var tenant: Person?
// Hellolad 被释放
// Apartment 502 被释放
他们的关系图:
图片: The Swift Programming Language AutomaticReferenceCounting
官方建议:如果能够确定在访问时不会已被释放的话,尽量使用 unowned ,如果存在被释放的可能,那就选择用 weak 。
闭包的循环引用(Closure/Block)
我们上面知道了如果我们在两个实例里互相引用了对方,会造成循环引用,而我们使用闭包捕获了self之后,也可能会造成循环引用,我们来看Swift是如何发生这种问题,以及应该怎样去解决这样的问题?
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) 被释放")
}
}
在HTMLElement
我们定义了三个属性,其中asHTML
是一个懒加载的闭包属性,并且返回()-String的一个函数。我们想要打印出来像这种样式的html语句:<h1>Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad</h1>
我们来实践一下:
let heading = HTMLElement(name: "h1")
let defaultText = "Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
heading = nil
// 打印: <h1>Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad Hellolad</h1>
// h1 被释放
我们为heading.asHTML赋值,并且打印他,发现h1
已经被释放,因为它并没有走asHTML的存储属性的get函数,所以没有捕获到self
,因此并不会造成循环引用
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello lad!!")
print(paragraph?.asHTML())
paragraph = nil
// 打印:"<p>Hello lad!!</p>"
只打印了"<p>Hello lad!!</p>"
但是p
确并没有被释放,它走了该asHTML存储属性的get函数,该闭包已经造成了一个循环引用。
图片: The Swift Programming Language AutomaticReferenceCounting
解决闭包的循环引用
我们知道了闭包造成的循环引用,我们依然需要用weak
或unwoned
来打破循环强引用,我们知道weak
是当被修饰的属性被置为nil之后,ARC就会帮助我们销毁该引用,并将retainCount记为0。然而我们的闭包持有了self
,而self
又持有了闭包,所以self是不可能为nil的,如果为nil,那我们调用该asHTML将会永远crash,所以我们最合适的方式是使用unowned
。
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name)/>"
}
}
他们之间的引用关系如下:
图片: The Swift Programming Language AutomaticReferenceCounting
这样的话 闭包内的self
对于闭包来说就是一个无主的。我们打印出来看:
paragraph = nil
// p 被释放
参考文献&图片来源:
1. The Swift Programming Language AutomaticReferenceCounting
2. What Is Automatic Reference Counting (ARC)