Yuchen
Yuchen

Reputation: 33036

Why do we need to call "disposeBy(bag)" explicitly after "subscribe" in RxSwift

I read about this from a blog post http://adamborek.com/memory-managment-rxswift/:

When you subscribe for an Observable the Disposable keeps a reference to the Observable and the Observable keeps a strong reference to the Disposable (Rx creates some kind of a retain cycle here). Thanks to that if user navigates back in navigation stack the Observable won’t be deallocated unless you want it to be deallocated.

So purely just for understanding this, I created this dummy project: where is there a view, and in the middle of the view, there is a giant button which will emit events about how many times the button is tapped on. Simple as that.

import UIKit
import RxCocoa
import RxSwift

class Button: UIButton {
    private var number: Int = 0

    private let buttonPushedSubject: PublishSubject<Int> = PublishSubject.init()
    var buttonPushedObservable: Observable<Int> { return buttonPushedSubject }

    deinit {
        print("Button was deallocated.")
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    }

    @objc final func buttonTapped() {
        number = number + 1
        buttonPushedSubject.onNext(number)
    }
}

class ViewController: UIViewController {
    @IBOutlet private weak var button: Button!

    deinit {
        print("ViewController was deallocated.")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        button.buttonPushedObservable.subscribe(onNext: { (number) in
            print("Number is \(number)")
        }, onError: nil, onCompleted: nil, onDisposed: nil)
    }
}

And surprisingly, after I close this view controller, the logs look like this:

Number is 1
Number is 2
Number is 3
Number is 4
ViewController was deallocated.
Button was deallocated.
...

which means both ViewController and the Button have been released! In this case, I didn't call the disposeBy(bag) and the compiler giving warning.

enter image description here

Then I started looking at the implementation of subscribe(onNext:...) (c/p below):

let disposable: Disposable

if let disposed = onDisposed {
    disposable = Disposables.create(with: disposed)
}
else {
    disposable = Disposables.create()
}

let callStack = Hooks.recordCallStackOnError ? Hooks.customCaptureSubscriptionCallstack() : []

let observer = AnonymousObserver<E> { event in

    switch event {
    case .next(let value):
        onNext?(value)
    case .error(let error):
        if let onError = onError {
            onError(error)
        }
        else {
            Hooks.defaultErrorHandler(callStack, error)
        }
        disposable.dispose()
    case .completed:
        onCompleted?()
        disposable.dispose()
    }
}
return Disposables.create(
    self.asObservable().subscribe(observer),
    disposable
)

In this block of code above, it is true that observer holds a strong reference to disposable through the lambda function. However, what I don't understand is that how does disposable holds a strong reference to observer?

Upvotes: 3

Views: 1998

Answers (1)

Daniel T.
Daniel T.

Reputation: 33967

While the observable is active there is a reference cycle, but the deallocation of your button sends a complete event which breaks the cycle.

That said, if you do something like this Observable<Int>.interval(3).subscribe() the stream will not deallocate.

Streams only shutdown (and thus deallocate) if the source completes/errors or if dispose() is called on the resulting disposable. With the above line of code, the source (interval) will never complete or error and no reference to the disposable was kept so there is no way to call dispose() on it.

The best way to think of it is this... complete/error is the source's way of telling the sink that it is done emitting (which means the stream is no longer needed,) and calling dispose() on the disposable is the sinks way of telling the source that it isn't interested in receiving any more events (which also means the stream is no longer needed.) In order to deallocate the stream, either the source or sink needs to report that it's finished.

To explicitly answer your question... You don't need to add the disposable to a dispose bag, but if the view controller deletes without disposing and the source doesn't send a complete message, the stream will leak. So safety first, make sure the sink disposes when it's done with the stream.

Upvotes: 2

Related Questions