【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 は非常に強力なツールであると感じるとともに難しさを感じる部分もあります。

サイボウズ版 MySQL パフォーマンスチューニングとその結果

こんにちは、アプリケーション基盤チームの青木(@a_o_k_i_n_g)です。先日親知らずを抜歯した時、つらすぎたので MySQL の JOIN のことを考えて心の平静を保っていました。

サイボウズの製品のひとつである kintone はニーズに応じて自由に業務アプリのようなものを手軽に作ることができ、データの検索条件やソート条件も細かくカスタマイズ可能で、様々なレベルでのアクセス権も設定可能という非常に便利なツールです。

しかしその機能を支える裏側では複雑なクエリが発行され、MySQL に多大な負荷をかけています。サイボウズのクラウドには数十テラバイトに登る MySQL データがあり、数千万件オーダーのテーブルを複数 JOIN するクエリが毎秒のように実行されるという、エンジニア魂が滾る環境です。

現在サイボウズでは性能改善に力を入れており、僕もその業務に従事しています。例えば2018年7月の更新では kintone の Innodb_rows_read を半減させており、その成果は記事の最後で紹介しています。

サイボウズの性能改善業で得た MySQL に関する知識知見をこの記事にまとめました。MySQL の性能に苦しむ皆様のお役に立てれば幸いです。

続きを読む

PSIRT Framework のご紹介

PSIRT Framework のご紹介

こんにちは。セキュリティ室の伊藤です。 昨年から取り組んできた PSIRT Framework(ドラフト版)を日本語に抄訳した成果物が公開されましたので、 本ブログで、ご紹介いたします。

PSIRT Services Framework 1.0 Draft (Japanese) ※ PDF ファイルです。 https://www.first.org/education/FIRST_PSIRT_Services_Framework_v1.0_draft_ja.pdf

続きを読む