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

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

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

# 内存泄漏

# 未调用 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.

可以看到控制台并没有打印 CompletedDisposed , 整个事件流并没有释放,同时 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 (opens new window) 会更简洁一些。

这段代码是从富强大大的Swift 实践初探 (opens new window)摘来的代码,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: "https://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 复用问题,这属于重复订阅问题