不管在什么语言里,内存管理的内容都很重要,所以我打算花比较长的篇幅仔细的说说这块内容。
Swift 是自动管理内存
的,这也就是说,我们不再需要操心内存的申请和分配。当我们通过初始化创建一个对象时,Swift 会替我们管理和分配内存。而释放的过程遵循了自动引用计数
(ARC)的规则:当一个对象没有引用的时候,其内存会被自动回收。这套机制很大程度上简化了我们的编码,我们只需要保证在合适的时候将引用置空(比如超过作用域
,或者手动设为nil
等),就可以确保内存使用不出现问题。
但是,所有的自动引用计数机制都有一个从理论上无法绕过的限制,那就是循环引用
(retain cycle)的情况。
什么是循环引用
为了更清晰的解释Swift 中的循环引用的一般情况,这里还是简单进行说明。假设我们有两个类A 和B,它们之中分别有一个可以持有对方的存储类型的属性:
class A {
let b: B
init() {
b = B()
b.a = self
}
deinit {
print("A deinit")
}
}
class B {
var a: A? = nil
deinit {
print("B deinit")
}
}
在A 初始化
方法中,我们生成了一个B 的实例
并将其存储在属性
中。然后我们又将A 的实例赋值给了b.a
。这样a.b
和b.a
将在初始化的时候形成一个引用循环
。现在当第三方的调用初始化了A,然后即使立即将其释放,A
和B
两个类实例的deinit 方法
也不会被调用,说明它们并没有被释放
。
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var obj: A? = A.init()
obj = nil
// 内存没有释放
}
}
因为即使obj
不再持有A
的这个对象,B
中的b.a
依然引用着这个对象,导致它无法释放。而进一步,a
中也持有
着b
,导致b 也无法释放。在将obj 设为nil
之后,我们在代码里再也拿不到对于这个对象的引用了,所以除非“杀”掉整个进程,否则我们永远无法 将它释放
了。
在Swift 里防止循环引用
为了防止这种“人神共愤”的“悲剧”的发生,我们必须给编译器一点提示,表明我们不希望它们互相持有。一般来说我们习惯希望“被动”的一方不要去持有“主动”的一方。在这里,b.a
里对A 的实例的持有是由A 的方法设定的,我们在之后直接使用的也是A 的实例,因此认为b
是被动
的一方。我们可以将上面的class B
的声明改为:
class B {
weak var a: A? = nil
deinit {
print("B deinit")
}
}
在var a
前面加上了weak
,向编译器说明我们不希望持有a
。这是,当obj 指向nil
时,整个环境中就没有对A 的这个实例的持有了,于是这个实例可以得到释放。接着,这个被释放的实例上对b 的引用a.b
也随着这次释放结束了作用域
,所以b 的引用也将归零
,得到释放。添加weak 后的输出如下:
A deinit
B deinit
可能有心的朋友已经注意到,在Swift 中除了weak
以外,还有另一个冲着编译器“叫喊”这类似“不要引用我”的标识符,那就是unowned
。它们的区别在哪里呢? 如果你是一直写Objective-C
过来的,那么从表面的行为上来说unowned 更像
以前的unsafe_unretained
,而被释放了,它仍然会保持
对被已经释放了的对象的一个“无效的”引用
,它不能是Optional 值
,也不会
被指向nil
。如果你尝试调用这个引用的方法或者访问成员属性的话,程序就会奔溃
。而weak
则友好一些
,在引用的内容被释放后,标记为weak 的成员
将会自动的变成nil
(因此被标记位 @weak
的变量一定
是Optional 值
)。关于两者使用的选择,Apple 给我们的建议是:如果能够确定在访问时不会已经释放的话,就尽量使用unowned
;如果存在被释放的可能,那就选择用weak
。
我们结合现实中的编码使用来看看如何选择吧。日常工作中使用弱引用的最常见的场景有两个:
- 设置
delegate
时 - 在
self
属性存储为闭包,其中有对self
的引用时。
前者是Cocoa 框架的常见设计模式,比如我们有一个负责网络请求的类,它实现了发送请求及接收请求结果的任务,这个结果是通过实现请求类的protocol 的方式
来实现的,这个时候我们一般设置delegate 为weak
:
@objc protocol RequestHandler {
@objc optional func requestFinish()
}
class RequestManager: RequestHandler {
func requestFinish() {
print("请求完成")
}
func sendRequest() {
let req = Request()
req.delegate = self
req.send()
}
}
class Request {
weak var delegate: RequestHandler!
func send() {
// 发送请求
// 一般来说会将 req 的引用传递给网络框架
print("发送请求")
}
func gotResponse() {
// 请求返回
delegate.requestFinish?()
}
}
req
中以weak
的方式持有了delegate
,因为网络请求是一个异步过程,很可能会遇到用户不愿意等待而选择放弃的情况。这种情况下一般都会将RequestManager
进行清理,所以我们无法保证在拿到返回时作为delegate
的RequestManager
对象一定存在。因此我们使用了weak
而非 unowned
,并在调用前进行了判断。
闭包和循环引用
另一种闭包的情况稍微复杂一些。我们首先要知道,闭包中对任何其他元素的引用都是会被闭包自动持有的。如果我们在闭包
中写了self
这样的东西的话,那我们其实也就在闭包
内持有
了当前的对象
。这里就出现了一个实际开发中比较隐蔽的陷阱:如果当前的实例直接或者间接的对这个闭包又有引用的话,就形成了一个self -> 闭包 -> self
的循环引用
。最简单的例子是,我们声明了一个闭包用来以特定的形式打印self 中的字符串:
class Person {
let name: String
lazy var printName: () -> () = {
print("The name is \(self.name)")
}
init(name: String) {
self.name = name
}
deinit {
print("Person deinit \(self.name)")
}
}
let person = Person.init(name: "XiaoMing")
person.printName()
// 输出
// The name is XiaoMing
printName
是sel
f 的属性
,会被self 持有
,而它本身又在闭包
内持有self
,这导致了person
的deinit
在自身超过作用域后没有被调用
,也就是没有被释放
。为了解决这种闭包内的循环引用,我们需要在闭包开始
的时候添加一个标注
,来表示这个闭包内某些要素应该以何种特定的方式来使用。我们可以将printName 修改
为这样:
lazy var printName: () -> () = { [weak self] in
if let strongSelf = self {
print("The name is \(strongSelf.name)")
}
}
现在内存释放就正确了:
// 输出:
// The name is XiaoMing
// Person deinit XiaoMing
如果能确定在整个过程中self
不会被释放的话,我们可以将上面的weak 改为unowned
,这样就不再需要strongSelf 的判断
。但是如果在过程中self 被释放了而printName 这个闭包没有被释放的话(比如生成Person 后,某个外部变量持有了printName,随后这个Person 对象被释放了,但是printName 已然存在并可能被调用),使用unowned
将造成崩溃
。在这里我们需要根据实际的需求
来决定使用weak
还是unowned
。
这种在闭包参数的位置进行标注的语法结构是将要标注的内容放
在原来参数的前面
,并使用中括号
括起来。如果有多个需要标注的元素的话,在同一个中括
号内用逗号隔开
,举个例子:
// 标注前
let closed: (Int) -> Bool = { (number) in
// ...
return true
}
// 标注后
let closed: (Int) -> Bool = { [unowned self, weak someObject] (number) in
// ...
return true
}