guard let self = self else { return }
您也可以 在 bilibili 阅读此文,或者了解 白学
没错,这段代码的意思就是“老子就是老子”!让我们一起来看看这段莫名其妙的代码究竟有什么奥秘。
首先,它是要解决什么问题呢?当多个对象之间相互持有强引用的话,那么谁都无法从中被释放,这是非常糟糕的情况,我们通常称为 修罗场 引用循环。那么在 Swift 里的引用循环是什么样的呢?我们可以看看 Memory Graph(Xcode 自带)所创作的艺术作品:
还有更夸张的:
Sometimes you have small memory leaks, sometimes you accidentally create a retain-cycle fractal universe pic.twitter.com/fA9KylCzqo
— Oskar Groth (@oskargroth) March 15, 2019
为什么会变成这样呢?
Swift 中的引用方式有三种:强引用、弱引用(weak
)和无主引用(unowned
)。具体的可以看《The Swift Programming Language》里关于 Automatic Reference Counting(自动引用计数)的介绍,不过简单总结一下的话就是
- 强引用:默认,抓着其他对象不放,直到自己的生命结束
- 弱引用:佛系引用,对象在就有值,对象没了就是
nil
,所以必须是可选类型 - 无主引用:强制解包的弱引用,对象在一切安好,对象没了直接崩溃
所以当两个对象互相持有对方的强引用的时候,比如下面文件夹和文件的例子:
1 | class Folder { |
就会导致大家都等着对方先放手,直到天荒地老(程序耗尽内存停止运行),谁也逃不掉。
我怎么知道有没有引用循环?
最简单的方法:能不写 self
的时候就不写,当 Xcode 提示必须加上 self
,比如下面这个错误示范里面这样,那就是可能会造成引用循环:
1 | class SomeTableViewController: UITableViewController { |
但是,上面那个文件系统的例子导致的引用循环就不能通过这种方式发现,所以我们需要用到 Memory Graph。运行你的程序之后,点击这个按钮就能看到对象之间的引用情况:
有时甚至看 memory graph 也没办法搞明白究竟是什么导致了内存泄漏,可以看看 CS193p 或 WWDC 了解如何使用 Instruments 这个复杂但是功能强大的软件。
我知道错了,我下次还敢?
最偷懒的解决方式就是,每次 Xcode 让你加 self
的时候就听它的,但你还另外在闭包的最开始加上这行全是关键字的代码:
1 | [weak self] in guard let self = self else { return } |
它会先弱引用 self
,然后确认我们能够暂时强引用 self
,在闭包运行结束之后就释放对 self
的引用,这样就能解决大部分闭包导致的引用循环了!不过,如果出现多层闭包嵌套(甚至是 callback hell)的情况呢?
1 | // 很常见的代码,后台下载数据 |
这样写是正确的吗?并不,因为 async
的闭包已经把 self
转为了强引用,所以 animate
的闭包用的 self
也是强引用的,有可能造成引用循环。所以应该改写成这样:
1 | URLSession.shared.dataTask(with: URL(string: "AZ.png")!) { |
你以为这样就完了?naïve!因为 SR-3805 里提到的编译器的行为(或者叫做一般人不知道的坑),为了能够让内层的闭包弱引用 self
,外层的闭包默认强引用了 self
,也就是说 dataTask
里的 self
是强引用。所以(非常容易不小心写错的)正确写法是:
1 | URLSession.shared.dataTask(with: URL(string: "AZ.png")!) { |
注意 [weak self]
是在最外层的闭包声明的,但 guard let
是在第二层闭包才有。
最后想说的是,上面都只是为了示范,其实如果可以的话,就一直用 weak self 也是没有问题的。避免了 guard let 也避免了很多麻烦,不过并不是什么时候都能这么用的:
1 | URLSession.shared.dataTask(with: URL(string: "AZ.png")!) { |
等等,刚才的三角关系呢?
稍微有些复杂,因为我们期望的是当文件被移除的时候文件夹不会死拽着不放,所以我们需要数组能够存储弱引用。因此,我们需要引入一个新的类型来包装一下:
1 | class WeakBox<T: AnyObject> { |
这样,当一个文件(File)实例消失之后,其文件夹的数组里只会留下包装用的 WeakBox
(value
自然是 nil
)。
但其实更常见的问题是不正确的代理模式导致引用循环(比如 View 和 Controller 之间的通信)。下面这个例子中,根目录(root)和文件系统(fileSystem)之间存在循环引用:
1 | protocol FolderWatcher { |
而正确的代理模式应该弱引用 delegate:
1 | - protocol FolderWatcher { |
注意新增的 AnyObject
要求,因为只有类的实例能够被引用,值类型是不行的哟~