最近在开发macOS版的咕唧2,遇到了视图控制器没有释放的问题。花了些时间进行调试。结果总结如下:
我们知道,如果存在循环引用,就会导致类无法释放。因此,我们通常会使用weak var delegate
的方式来避免循环引用。
这里不谈一般的循环引用,谈谈closure
的循环引用。
closure的循环引用
如图,点击上面视图控制器的Next
按钮,会自动弹出下面的视图控制器。代码如下:
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
import Cocoa
class V2ViewController: NSViewController {
private let a = 100
private var foo:Foo!
override func viewDidLoad() {
super.viewDidLoad()
foo = Foo(doStaff)
foo.run()
}
deinit {
print("V2ViewController deinit.")
}
func doStaff() {
print(self.a)
}
}
class Foo {
private(set) var bar:() -> ()
init(_ bar:@escaping () -> ()) {
self.bar = bar
}
deinit {
print("Foo deinit.")
}
func run() {
bar()
}
}
运行并关闭弹出的视图控制器,控制台没有输出deinit的打印。存在内存泄漏。
分析
这是因为Foo
中的bar
,引用了V2ViewController
的doStaff
。而doStaff
引用了self
。
解决
对于closure
,我们不能使用weak
,只能使用其它的方式。
方法1
调用完成后手动释放。
func run() {
bar()
bar = {}
}
方法2
还是手动释放,不过将bar定义为(()->())?
,即Opitional
。
class Foo {
private(set) var bar:(() -> ())?
init(_ bar:@escaping () -> ()) {
self.bar = bar
}
deinit {
print("Foo deinit.")
}
func run() {
bar?()
bar = nil
}
}
方法3
更改调用的方式,不直接分配函数,而是增加一层closure
。
override func viewDidLoad() {
super.viewDidLoad()
foo = Foo({ [unowned self] in
self.doStaff()
})
foo.run()
}
通常我们认为直接分配和增加一层是等价的。但是这里我们看到,实际上,二者并不完全等价。
结论
综合起来,我认为方法3的解决方案,修改代码的成本是最低的。因为它是直接从源头解决了问题。