起因
来水一篇文章。。。事情的起因是笔者在开发自测是发现了一处内存泄漏。排查到最后发现有一处用RxSwift实现的代码对ViewController强引用了,导致了循环引用。代码的写法与下列代码类似:
class ViewController: UIViewController {
private let a: PublishSubject<Int> = PublishSubject<Int>.init()
private let bag: DisposeBag = DisposeBag.init()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
a.asObserver().subscribe { event in
print(event.element ?? 0)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in
print((self?.description ?? "") + "asyncAfter")
}
}.disposed(by: bag)
}
deinit {
print(self.description + "deinit")
}
}
最终形成了一个环。解决的方法就是可以在subscribe方法的闭包上声明[weak self]的捕获。由于代码的时间比较长,有可能是当时认为该闭包没有使用到self,所以就没有特意声明。这个也可以说是笔者犯的一个低级错误了,也借此机会填补一下闭包上下文捕获的知识盲区。
所以本文将记录:
Swift闭包的上下文捕获的逻辑- 以此衍生出是否什么闭包都需要
[weak self]。
问题解析
分析
a.asObserver().subscribe { event in
print(event.element ?? 0)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { [weak self] in
print((self?.description ?? "") + "asyncAfter")
}
}.disposed(by: bag)
先解决笔者遇到的这个问题,代码中涉及到两个闭包的嵌套。造成泄漏的点在于DispatchQueue.main.asyncAfter中的闭包有对于self(ViewController实例)的捕获,导致了外层的subscribe中的闭包也需要捕获self。而self在外层闭包并没有声明weak,导致了该闭包对于self的引用是强引用的。
ObservableType#subscribe的闭包为逃逸闭包GCD的DispatchQueue#asyncAfter的闭包也是逃逸闭包
所以就导致了闭包与self间接形成循环引用的问题。
验证
通过对a发送一个信号a.onNext(1)触发上述的闭包。在subscribe的闭包处打上断点
此时,闭包内可访问的变量有:
- 闭包的参数
event self
那么如果将代码更改为:
a.asObserver().subscribe { event in
print(event.element ?? 0)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { //[weak self] in
// print((self?.description ?? "") + "asyncAfter")
print("asyncAfter")
}
}.disposed(by: bag)
然后还是发送一个信号a.onNext(1)触发上述的闭包。在subscribe的闭包处打上断点。
此时闭包内可访问的变量就只剩下闭包的参数event。
这样就说明,是由于GCD的DispatchQueue#asyncAfter的闭包需要用到self,从而导致外层的闭包也需要隐式的捕获self。最终导致上述那个非常对称的环产生。
闭包的上下文捕获
隐式捕获
笔者遇到的问题,其实就是一个隐式捕获的问题。这里借用文档中的例子,在incrementer闭包调用时打上断点。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
闭包中可访问的变量为makeIncrementer方法中的runningTotal和amount。说明它隐式捕获的这两个变量。
造成笔者遇到的问题,其实还有一个前提:闭包需要是逃逸闭包。有以下代码:
private let b: Int = 1;
make {
print(b)
}
func make(closure: () -> Void) {
closure()
}
- 变量
b为该类的全局变量 make方法中传入闭包closure,closure为非逃逸闭包。
closure闭包内的print(b)其实与print(self.b)等价。原因是因为闭包已经隐式捕获的self。如果对上述代码稍作修改
private let b: Int = 1;
make {
print(b)
}
func make(closure: @escaping () -> Void) {
closure()
}
就会报错:Reference to property 'b' in closure requires explicit use of 'self' to make capture semantics explicit。大概意思就是,需要使用self.b的写法来明确捕获语义。
make {
print(self.b)
}
需要明确的是,不管是逃逸还是非逃逸,在这种写法下都是隐式捕获了self,只是编译器在逃逸闭包上作了约束,必须显示的使用self,目的应该是为了规范开发者避免循环引用的发生。Swift的官方文档中也有类似的描述。
显式捕获
显式捕获很好理解,就比如经常接触的[weak self]。当然,显式捕获可以选择您在闭包中使用到的变量(不一定是self),变量一般为全局变量。还是上述的例子,可以修改为
private let b: Int = 1;
make { [b] in
print(b)
}
func make(closure: @escaping () -> Void) {
closure()
}
这样closure闭包就只会捕获到变量b,而不会捕获到self,除非您在闭包中使用了self,那就又是一个隐式捕获了。
说回最常见的[weak self]。无论是显式捕获还是隐式捕获以下两种写法是等价的:
// 隐式捕获
make {
print(self.b)
}
// 显式捕获
make { [self] in
print(self.b)
}
[weak self]的意义是显式捕获一个弱引用的self,以此来保证不会因为闭包对于self的持有形成的循环引用而无法正常回收。(ps:注意,这里说的是循环引用,后面会在什么情况下使用[weak self]聊到这个事情)。
除了[weak self]还有一个[unowned self],但是这个是一个比较危险的用法。可以参考:
什么闭包都需要[weak self]吗?
其实这种做法是错误的,上述聊到逃逸闭包的时候,出现了一个编译器要求我们显式调用self的报错。所以事情应该是这样的:
- 由于闭包被声明为逃逸,所以它有被某个对象保存为全局变量的可能。(ps:注意,这里只是可能)。
- 无论是隐式捕获还是显式,实际上也是捕获了,比如捕获了
self。也就是闭包持有了该对象的引用。 - 由于捕获的对象不一定是
self,所以不一定是声明[weak self],可以是[weak abc]。 - 造成内存泄漏的罪魁祸首还是循环引用,所以只要闭包和
self(或者self中的其他全局变量)的引用没有形成环,就不会存在泄漏。
基于以上的总结,[weak self]这种写法,其实只是为了避免逃逸闭包作为了一个对象(如:self)的全局变量,而闭包本身又捕获了(持用了)该对象这种循环引用问题。
做一个合格的非逃逸闭包
上述已经分析了逃逸闭包在使用到self(或其他全局变量)时,需要声明[weak xxx]的原因。那是不是就意味着非逃逸闭包就没有这种问题呢?答案是否定的。原因是因为需要看闭包的使用情况,如果非逃逸闭包后续会传递给逃逸闭包的话,那么还是存在一样的情况。
let d = {
print(self.b)
}
make(closure: d)
func make(closure: @escaping () -> Void) {
}
上面的代码,闭包d就不是一个合格的非逃逸闭包。
总结
本文主要通过笔者遇到的一次内存泄漏,引申出Swift闭包的上下文捕获问题。简单介绍了闭包的隐式捕获和显式捕获。[weak self]并不适合所有情况,只有是闭包被作为了全局变量与对象相互持有了引用的情况下,才需要显式使用[weak self]来解决循环引用的问题。当然,[weak self]中不一定是self,可以按需缩小需要捕获的范围,优化对象的释放问题。
参考文章: