前几天看了美团的 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
。
有两种常用的方式解决上述问题。
.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 解决该问题。
首先需要 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
中。
这是一个在 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 不适合仅用来处理异步。
仍然以上面 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
等操作符完成更加复杂的业务逻辑。这一点,本文就不在这里赘述了,这不是本文的重点。