Swift Tips 022 - Overriding self with a weak reference

339 阅读4分钟

代码截图

代码出处: Swift Tips 022 by John Sundell

小笔记

这段代码在说什么

在处理逃逸闭包内部的逻辑时,我们通常会使用 weak self 的方式来避免循环引用。为了在闭包里面正确的使用 self 变量,我们需要通过可选绑定的方式将原先的 self 重新命名并使用。

在 Swift 4.2 之前,self 是全局保留关键字,所以如果在逃逸闭包中把 self 标记为 weak 后,还想继续使用 self 就需要使用 ` 将 self 包起来:

guard let `self` = self else { return }

而在 Swift 4.2 之后,基于 Allow using optional binding to upgrade self from a weak to strong reference 提案,可选绑定中的 self 不再作为保留关键字。我们完全可以光明正大的这么写了:

guard let self = self else { return }

内存泄漏与闭包里的 self

在开始下面的内容前,我们先回顾以下内存泄漏和闭包里的 sefl 这个话题。

假设我们有这样一段代码:

doSomething(then: {
  // do something else
})

由于 Swift 的内存管理机制,如果我们在闭包里面使用一些局部变量的话,闭包就会捕获这个变量,就像下面的代码一样:

var dog = Dog()
doSomething(then: {
  dog.bark() // dog must be captured so it will live long enough
})

Swift 编译器会自动管理 dog 的引用计数,这个 dog 会被 doSomething 的闭包 捕获,从而增加引用计数,即使跳出这段代码的作用域,dog 实例仍然会被 doSomething 的闭包所持有。

这种形式在上面的例子中似乎并无大碍,但有时候却会带来一些问题。最常见的一种情况就是循环引用。

试想一下:某个实例拥有一个闭包属性,而这个属性又会调用自身的一些实例方法或者属性时,它们就会产出如下图所示的引用关系。

根据前面的例子,我们知道只要闭包不被销毁,它就会一直持有 self,而另一边,闭包作为实例的一个属性,它们是同生死,共存亡!

你瞧,它们谁也无法释放对方......然后,内存泄漏了!

避免循环引用

为了避免循环引用,Swift 提供了捕获列表来让开发者决定如何持有相关变量。拿之前的 dog 代码举例说明,它就变成了如下的样式:

let dog = Dog()
doSomething(then: { [weak dog] in
    // dog is now Optional<Dog>
    dog?.bark()
)

此时,当我们再次跳出这段代码的作用域后,闭包里 dog 将变为 nil。除非我们还在其他地方,通过一些途径强持有了 dog 实例。

同理,捕获列表也支持 self:

doSomething(then: { [weak self] in
    self?.doSomethingElse()
)

不过由于此时的 self 变成了一个可选类型,所以如果我们要区分不同状态下的代码行为,我们通常还会在闭包内部创建一个对 weak self 的强引用。

doSomething(then: { [weak self] in
    guard let strongSelf = self { else return }
    strongSelf.doSomethingElse()
)

在上面的这个例子中,我们确保了 strongSelf 是一定有值的,而且当 self 为 nil 的时候,不会执行剩下的代码。

可选绑定里的 self

虽然在许多 Swift 项目中能够看到 strongSelf 的写法,但开发者还是觉得这种方式有点不那么时髦,总想着要弄点什么新鲜玩意儿来!

果不其然,Swift 社区里的人发现了在闭包里还可以进行如下的操作:

guard let `self` = self else { return }

这种 'trick' 的方式能够让我们统一 self 的写法,也让代码看起来变得更顺眼了一些。

但有意思的是,我们的 Chris,也就是 Swift 之父,亲口承认了这个所谓的“新特性”,其实是一个 Swift 编译器的 bug

但社区里的开发者并没有打算放弃努力!

终于,在 Swift 4.2 的时候,Swift 团队接受了开发者提出的相关 proposal 并提供了在可选绑定中不再将 self 作为保留关键字的特性。所以我们现在能够看到如下的写法:

guard let self = self else { return }

当然取消了这个限制后也意味着 self 可能不一定是 self 了:

var number: Int? = nil
if let self = number {
    print(self) // 这里的 self 是 number:Int
}

所以,即使如此,还是希望大家在正确的地方将 self 作为可选绑定的变量名,免得造成其他的困扰。