【RxSwift】Singleton で DisposeBag を使うことの考察

こんにちは。モバイル開発チームに所属している小島です。

弊社のプロダクトでもようやく RxSwift を使い始めています。今回は RxSwift の Disposable について思うところがあったので、メモしておきます。

Disposable と DisposeBag

Observable を subscribe すると、Disposable を返してきます。 Disposable は、subscribe したものを unsubscribe するための仕組みで、これを無視すると subscribe した処理が永遠に解放されずにメモリリークやリソースリークに繋がる恐れがあります。 なので、とりあえず DisposeBag に入れておけばいいよというのはよく見かけます。

class SomeClass {
    private let disposeBag = DisposeBag()
    private let dependentObject: DependentClass

    func someMethod() {
        let disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        self.disposeBag.insert(disposable)
    }

    func someMethod2() {
        // 書き方が違うだけで↑と意味は同じ
        dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        .disposed(by: self.disposeBag)
    }
}

DisposeBag は、Disposable をためておいて、自身が解放 (deinit) されるときに保持している Disposable を dispose してくれます。上記の場合だと、SomeClass が解放されるときに disposeBag も解放されこれまで subscribe したものが unsubscribe されるという流れになります。

Singleton オブジェクトの場合も同じように考えていいのだろうか?

DisposeBag は、便利な仕組みではありますが、保持しているオブジェクトのライフサイクルと同一になりますので、Singleton オブジェクトの場合にはアプリのライフサイクルと同一ということになります。

class SingletonClass {
    private let disposeBag = DisposeBag()
    private let dependentObject: DependentClass

    func calledFrequently() {
        dependentObject.getObservable().subscribe(onNext: {
            // do someting.
        })
        .disposed(by: self.disposeBag)
        // disposeBag が解放されることはない
    }
}

例えば上記のような場合、calledFrequently が呼ばれるたびに disposeBag に Disposable が追加されますが、disposeBag は開放されないので、subscribe された Observable は unsubscribe されることはありません。

チーム内で改善策を決めたけど、期待通りに unsubscribe されないことがありました

チームメンバーとも相談し Singleton オブジェクトの場合は以下のような形にしようと決めました。

class SingletonClass {
    private let dependentObject: DependentClass

    func calledFrequently() {
        var disposable: Disposable?
        disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
            disposable?.dispose()
        })
    }
}

ところが上記の書き方の場合、とあるケースで期待通りの動作になりませんでした。 onNext で渡しているクロージャの実行タイミングは Observable によって異なっているのです。

class SingletonClass {
    private let dependentObject: DependentClass

    func method1() {
        var disposable: Disposable?
        disposable = dependentObject.getAsyncSubjectAsObservable().subscribe(onNext: {
            // do someting.
            print(disposable?)
        })
    }

    func method2() {
        var disposable: Disposable?
        disposable = dependentObject.getBeheiviorSubjectAsObservable().subscribe(onNext: {
            // do someting.
            print(disposable?)
        })
    }
}

上記のコードでは、method1 は AsyncSubject を Observable として受け取り、method2 は BeheiviorSubject を Observable で受け取る想定です。BeheiviorSubject を subscribe した時の onNext は同期的に呼び出されるので、method1 は disposable オブジェクトが出力されますが、method2 は nil が出力されます。 つまり method2 の場合では、想定通りに unsubscribe されず getBeheiviorSubjectAsObservable の中の BeheiviorSubject の値が変わるたびにクロージャが実行されてしまいました。

(正確には、method1 の場合でも getAsyncSubjectAsObservable の中で実行している AsyncSubject が同期的に onNext された場合は nil が出力されます)

問題点のまとめ

問題は onNext が同期的に呼び出されるのか、非同期で呼び出されるのか、呼び出し元にはわからないということです。都度、中身の実装を確認しに行くのは辛いですし、メソッド名を工夫してわかるようにするということも考えましたが、なんとなく微妙だなぁとも思いました。

今のところの改善案

チームメンバーと再度検討し、とりあえず以下のような形に落ち着きました。

class SingletonClass {
    private let dependentObject: DependentClass

    func useTake() {
        _ = dependentObject.getObservable().take(1).subscribe(onNext: {
            // do someting.
        })
    }

    func useCompositeDisposable() {
        let compositeDisposable = CompositeDisposable()
        let disposable = dependentObject.getObservable().subscribe(onNext: {
            // do someting.
            if (conditionalExpression) {
                compositeDisposable.dispose()
            }
        })
        _ = compositeDisposable.insert(diposable)
    }
}

そもそも onNext の処理が1回きりで良い場合は、take(1) を使って確実に1回で onCompolete が呼ばれるようにすることにしました。onCompolete が呼ばれれば unsubscribe の必要はないので、戻り値の Disposable は無視しても問題ありません。

onNext 内で dispose する必要がある場合は CompositeDisposable を使うようにしました。CompositeDisposable は、dispose を呼び出したときに insert してあった diposable を dispose してくれます。予め dispose を呼び出していた場合には、あとから insert した diposable は、即 dispose してくれます。これにより、onNext の実行が同期的か非同期的かは気にしなくても良くなります。

これがベストかはもう少し使ってみないとなんとも言えませんが、RxSwift は非常に強力なツールであると感じるとともに難しさを感じる部分もあります。