RxSwift 内存泄漏与资源释放/管理

Memory Leaks

前几天看了美团的ReactiveCocoa 中潜在的内存泄漏及解决方案 ,我也来试着写一下在使用 RxSwift 中可能存在的内存泄漏问题,以及对应的解决方案,同时讨论在 RxSwift 下如何进行资源的管理与释放,即解释 DisposeBag 。

考虑到现在自己很喜欢 RxSwift 中 Swift 3.0 分支的 API ,本文示例代码均基于 Swift 3.0 版本

内存泄漏

未调用 onCompleted

在 Rx 中,对一个 Model 进行监听是件非常麻烦的事情,但我们还是先试着写了一下。

class MTModel: NSObject {
dynamic var title: String
init(_ title: String) {
self.title = title
}
}

此时建立一个 Model ,为了支持 KVO ,需要继承 NSObject 同时添加 dynamic 标记(或者添加 @objc 标记)。

Observable.just(model)
.flatMap {
$0.rx.observe(String.self, "title")
}
.subscribe(onNext: { value in
if let value = value {
print("Title is \(value).")
}
}, onCompleted: {
print("Completed")
}, onDisposed: {
print("Disposed")
})
model
.rx
.deallocated
.subscribe(onNext: {
print("Model deallocated")
})
model.title = "111"
model.title = "222"

首先对应的 ViewController 已经释放了,这点和 ReactiveCocoa 2.5 不同,KVO 观察时,持有者并非当前的 ViewController 。但这里尴尬的是打印结果。

Title is title.
Title is 111.
Title is 222.

可以看到控制台并没有打印 Completed 和 Disposed , 整个事件流并没有释放,同时 model 也没有被释放(即没有打印 Model deallocated)。

这是正常的,因为一个 KVO ,即 rx.observe 是一个无限序列,本身自己并不会发射 Completed 。

有两种常用的方式解决上述问题。

.takeUntil(rx.deallocated)

.flatMap {
$0.rx.observe(String.self, "title")
}
.takeUntil(rx.deallocated)

在之前的 flatMap 后面添加 .takeUntil(rx.deallocated) 即可。

此时打印结果为。

Title is title.
Title is 111.
Title is 222.
Completed
Disposed
Model deallocated

相关的资源都已经释放。

DisposeBag

此外我们还可以通过添加 DisposeBag 解决该问题。

首先需要 ViewController 持有一个 disposeBag 。

let disposeBag = DisposeBag()

最后在订阅的结尾添加 .addDisposableTo(self.disposeBag) ,完整代码如下。

Observable.just(model)
.flatMap {
$0.rx.observe(String.self, "title")
}
.subscribe(onNext: { value in
if let value = value {
print("Title is \(value).")
}
}, onCompleted: {
print("Completed")
}, onDisposed: {
print("Disposed")
})
.addDisposableTo(self.disposeBag)

打印结果。

Title is title.
Title is 111.
Title is 222.
Model deallocated
Disposed

可以看到这里虽然没有打印 Completed ,但相关资源已经释放了。

没有打印 Completed 是正常的,因为整个事件流并没有人发射 Completed 。 当然,如果你认为 just 方法中发射了 Completed ,那也对,只是 flatMap 后的 Observable 是个无限的序列,自然也就轮不到 Completed 的传递了。

关于选择 DisposeBag 优于 takeUntil(rx.deallocated) 的讨论,我们将放到文章的第二部分,这里我们继续讨论内存泄漏问题。

闭包持有

这个就不需要多解释了,RxSwift 不像 ReactiveCocoa 2.5 版本使用了各种宏的黑魔法,所以出现循环引用一般都是写了 self 等情况。

原则上,self 应当只出现在 subscribe 中。

func 持有

这是一个在 Swift 中比较有意思的事情。

func foo(bar: Int) {
print(bar)
}

var foo : (bar: Int) -> () {
return { bar in
print(bar)
}
}

二者几乎是一样的。此时代码可以写成这个样子。

Observable.just(1)
.map { $0 + 1 }
.subscribe(onNext: foo)
.addDisposableTo(disposeBag)

所以才有这样一段有趣的代码。

tableView
.rx
.itemSelected
.map { (at: $0, animated: true) }
.subscribe(onNext: tableView.deselectRow)
.addDisposableTo(disposeBag)

然而,对于 foo 的那部分代码是可能存在循环引用的, foo 选择 func 实现时,会存在不知所措的循环引用。

这个暂时表示无解了。Orz

资源释放/管理

对于资源释放问题,最佳实践就是采用 DisposeBag 。

DisposeBag 会在其析构时释放持有的订阅者们,同时调用订阅者的 dispose 释放相关资源。

public final class DisposeBag: DisposeBase {
// ...
private func dispose() {
let oldDisposables = _dispose()
for disposable in oldDisposables {
disposable.dispose()
}
}
deinit {
dispose()
}
}

在创建每个 Observable 时,我们都可以在 dispose 时释放一些资源。比如 RxCocoa 中的网络请求,在释放资源时会 cancel 对应的 task Disposables.create(with: task.cancel) 。

public func response(_ request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { (data, response, error) in
// ...
observer.on(.next(data, httpResponse))
observer.on(.completed)
}

let t = task
t.resume()

return Disposables.create(with: task.cancel)
}
}

但需要注意的是,一般情况我们是不需要手动管理 DisposeBag

来看下面这部分代码。

private func reloadData() {
if disposable != nil {
disposable?.dispose()
disposable = nil
}
disposable = viewModel
.updateData()
.doOnError { [weak self] error in
JLToast.makeText("网络数据异常,请下拉重试!").show()
self?.refresher.stopLoad()
}
.doOnCompleted { [weak self] in
self?.refresher.stopLoad()
}
.subscribe()
}

不可以,这不可以,这样使用反而让代码维护更辛苦了,明明就是想刷新一下数据,却有 40% 的代码处理 disposable 了。项目逻辑复杂时,就会有一大堆 disposable ,这样的话不如使用 PromiseKit 会更简洁一些。

这段代码是从富强大大的Swift 实践初探摘来的代码,Orz 但愿我不会被打,拍个马屁,这篇文章对于 RxSwift 中的一些概念解释的还是很清晰的。补充,引入第三方框架,Carthage 可能是更好的选择。

当然上面的代码也可能会被写成这个样子。

private func reloadData() {
disposeBag = DisposeBag()
viewModel
.updateData()
.doOnError { [weak self] error in
JLToast.makeText("网络数据异常,请下拉重试!").show()
self?.refresher.stopLoad()
}
.doOnCompleted { [weak self] in
self?.refresher.stopLoad()
}
.subscribe()
.addDisposableTo(disposeBag)
}

此外上面这部分代码对于 doOn 的使用是比较不合理的。

我会在将来的文章中提到一些 doOn 的使用场景。

注,关于 RxSwift 和 PromiseKit 的区别,我将会在RxSwift vs PromiseKit 文章中进行探讨,我将解释为什么 PromiseKit 只是一个异步处理库,为什么 RxSwift 不适合仅用来处理异步。

正确理解 flatMap

使用 RxSwift 后,基本上就没有方法调用一说了。如果有,这基本不 Rx 。

逻辑源头

仍然以上面 reloadData 为例。一定有一个/多个 reloadData 的时机。比如,点击 Button ,下拉等。这里逻辑源头就是点击 Button 而非 reloadData 。

我们先以点击 Button 为例,画个图描述问题。

而原代码是

所以比较合理的代码写法应当是指出什么引起 reloadData ,通过链式调用将触发原因指出来。本例中通过 Button 点击触发数据更新。

button
.rx
.tap
.map { URL(string: "http://httpbin/org")! }
.flatMap(URLSession.shared.rx.JSON)
.subscribe { event in
switch event {
// ...
}
}
.addDisposableTo(disposeBag)

这段代码简单的描述了上面图中的逻辑,呈现一种流式的代码。我们可以将代码写成上面的样子,完全是多亏了 flatMap 这个操作符,通过返回一个 Observable 确保不论是异步执行代码还是同步代码,都能以链式的方式完成代码的书写。

此外 flatMap 还有两个兄弟方法,flatMapFirst flatMapLatest ,比如在网络请求未完成时,再次点击了 Button ,flatMapFirst 会忽略第二次点击 Button 的事件,不会进行网络请求;而 flatMapLatest 会取消第一次的网络请求,以第二次的网络请求覆盖掉。

如果有多个触发网络请求的情况,我们可以使用诸如 merge zip combineLatest 等操作符完成更加复杂的业务逻辑。这一点,本文就不在这里赘述了,这不是本文的重点。

总结

  • 一般对于一个 Observable ,记得在内部事件结束时调用观察者的 onCompleted 方法。
  • 注意闭包/ func 引起的循环引用。
  • 一般我们不需要手动管理内存释放问题,只需要在最后调用 .addDisposableTo(disposeBag) ,如果有需要手动释放 disposeBag 的情况,大多数都是逻辑上思考错了,尝试找到逻辑源头,减少调用方法的情况。(当然也有一些及特殊情况,比如 Cell 复用问题,这属于重复订阅问题
Like what you read? Give 靛青K a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.