blog
blog copied to clipboard
Swift 中使用weak避免对象循环引用
项目中经常会在Closure使用unowned 和 weak 修饰 self 对象避免循环引用所带来的坑
Closure里面会用到self的地方,一般都是这样处理
{[unowned self] in }
但是在一个网络请求的Closure回调里面,这样后来发现在这个Closure执行之前, self 已经被释放了
{[weak self] in
if let weSelf = self{
执行代码
}
}
swift 会使用ARC(Automatic Reference Counting)帮我们打理内存,但是在有些时候,尤其是对象间存在互相引用的时候,就会由于reference cycle导致内存无法释放,而我们也需要时刻保持清醒,来避免内存泄露
ARC是如何工作的呢?
Swift 使用“引用计数(reference count)”来管理类对象的生命周期,避免类对象在“仍被使用”的时候被意外释放。当一个对象的reference count为0时,Swift会立即删除该对象。
class Person {
let name: String
init(name: String) {
self.name = name print("\(name) is being initialized.")
}
deinit {
print("\(name) is being deinitialized.")
}
}
当执行以下的流程代码时:
var ref1: Person?
var ref2: Person?
ref1 = Person(name: "Mars") // count = 1
// Mars is being initialized.
ref2 = ref1 // count = 2
ref2 = nil // count = 1
ref1 = nil // count = 0
// Mars is being deinitialized.
Reference cycle(循环引用)又是怎么发生的呢?
对于Person这样的单个对象,ARC 可以很好的默默为我们工作。但是,当不同类对象之间存在相互引用时,指向彼此的是强引用,就会导致reference cycle, ARC无法释放它们之中的任何一个。
再看看下面的例子:
class Person {
let name: String
init(name: String) {
self.name = name print("\(name) is being initialized.")
}
deinit {
print("\(name) is being deinitialized.")
}
}
class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
print("Apartment \(unit) is being initialized.")
}
deinit {
print("Apartment \(unit) is being deinitialized.")
}
}
执行以下的代码
var mars: Person? = Person(name: "Mars") // count = 1
// Mars is being initialized
var apt: Apartment? = Apartment(unit: "11") // count = 1
// Apartment 11 is being initialized
mars!.apartment = apt11
// mars.count = 2
apt!.tenant = mars
// Set mars and apartment to nil
mars = nil
apt = nil
尽管我们把mars和apt设置为nil,Person和Apartmetn 的 deinit也不会被调用了。因为它们的两个member(apartment和tenant)是一个强引用,指向了彼此,让对象仍旧“存活”在内存里。但是,mars和apt已经被设置成nil,我们也已经无能为力了。这就是类对象之间的reference cycle。
Reference cycle的解决方案
- 当class member允许为nil时
Apartment 和 Person 中引起reference cycle数据成员都允许为nil,对于这种情况,我们可以像下面这样使用weak来解决 reference cycle:
weak var name: Type
class Apartment {
let unit: String
weak var tenant: Person?
// omit for simplicity...
}
var mars: Person? = Person(name: "Mars")
var apt: Apartment? = Apartment(unit: "11", owner: mars!)
mars!.apartment = apt
apt!.tenant = mars
mars = nil //由于已经没有strong reference指向**mars**,于是**mars**就被**ARC**释放了。
apt = nil // 这时,也没有任何strong reference指向apt了,它也会被ARC释放。
那么,我们是用weak还是unowned呢
在闭包和捕获的实例总是相互引用并且同时销毁时,将闭包内的捕获定义为unowned(无主引用)。 相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为 weak(弱引用)。
那什么情况下需要用到weak呢?
class ViewController: UIViewController {
var workItem = DispatchWorkItem(block: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.updateUI()
})
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now() + 2.33, execute: workItem)
}
func updateUI() {
// ...
}
}
上面是个延时操作的例子,如果声明为unowned(无主引用),在还没有到达延迟的时间的时候,我们 pop 掉这个 ViewController 的话,程序就会 crash。因为无主引用是非可选类型,pop 掉这个 ViewController 后,闭包中捕获的 self (也就是ViewController的实例)已经被销毁了,这时候再访问被销毁的实例,程序肯定会奔溃的。
所以这里应该声明为weak(弱引用),弱引用总是可选类型,当引用的实例被销毁后,弱引用的值会自动置为 nil,并且我们可以在闭包内检查它们是否存在。