Memory Leaks in RxSwift

Using RxSwift, I usually doubt whether generating memory leaks when binding a signal to binders, blocs, or functions. Finally, I decided to write an example listing all situations we usually use.

In the project blow, the NewViewController would be present from another view controller, touches the NewViewController will dismiss itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import UIKit
import RxCocoa
import RxSwift
import SnapKit

class MyButton: UIButton {
deinit {
print("Button deinit")
}
}

class NewViewController: UIViewController {

private let relay = BehaviorRelay(value: true)
private let bag = DisposeBag()
private var btn = MyButton()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
bindSignals()
}

private func bindSignals() {

}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
dismiss(animated: true, completion: nil)
}

deinit {
print("Controller deinit")
}
}

We will write codes in the function bindSignals to test our situations.

Bind single to binder

If the view component has the binder we need, we usually bind the signal to the binder. just like blow:

1
2
3
private func bindSignals() {
relay.bind(to: self.btn.rx.isHidden).disposed(by: bag)
}

When we bind the relay to the binder isHidden, we use the self in the expression, but it won’t lead to memory leaks when we click the view to dismiss the view controller.

Bind single to bloc

1
2
3
4
5
private func bindSignals() {
relay.bind(onNext: { hidden in
self.btn.isHidden = hidden
}).disposed(by: bag)
}

It’s clear that the bloc references the self and itself was referenced by the self too, so there is a cycle reference generated, we can add [weak self] or [unowned self] in the bloc to break the cycle reference.

Bind single to function

Above two situations is easy to understand. Some times you might not use the bloc but a function, just like blow:

1
2
3
4
5
6
private func bindSignals() {        
func hideBtnFunc(_ hidden: Bool) {
btn.isHidden = hidden
}
relay.bind(onNext: hideBtnFunc(_:)).disposed(by: bag)
}

When we write the local function bindBtnFunc, we don’t need to write the keyword self. But when we dismiss the view controller, we can not see any output in the console. The button and the view controller weren’t released. And you can’t write weak or unowned in a function to break the reference cycle.

Not only local functions but the class member functions also can not be used like this. If you still want to use a function, you must ensure that the function body doesn’t catch the self or any properties of the self. In the example above, you can declare a local variable referencing the self.btn. Then the function body will catch the local variable instead of the self.btn, without self, of course, no reference cycle.

1
2
3
4
5
6
7
8
private func bindSignals() {
let btn = self.btn
func hideBtnFunc(_ hidden: Bool) {
btn.isHidden = hidden
}

relay.bind(onNext: hideBtnFunc(_:)).disposed(by: bag)
}

Conclusion

Using binders is always safe. Using bloc maybe occur reference cycle, but you can use weak or unowned to break it. Using a function as an observer is dangerous. We usually overlook these situations. Unlike bloc, it always reminds us to take care of the reference cycles, and we normally think functions are safe. So do not use functions unless you ensure the function doesn’t use any properties from self.