こんにちは。きんくまです。
RxSwiftのメモリリークがどうなるのか気になったので調べてみました。
RxSwiftのバージョンは5.0.1
まずは実際のメモリリークをみてみます
こんな感じにメモリリークのマークが出ていることがわかります。ただし、これはSingleではなくてObservableを使っておこしています。理由はあとで説明。
前提
どういうときにおきるのか?いろいろと調べてみたのですが、通常は2点あります。
1. .disposed(by: disposeBag) を呼んでいない
2. クロージャーの中で weak self を使わずに self を使って循環参照をおこしてしまっている
逆にどういうときにObservableのメモリが開放されるかというと以下のとき
1. .onCompleted()を呼んだとき
2. .onError(error)を呼んだとき
参考)【RxSwift】Singleton で DisposeBag を使うことの考察
Singleはメモリリークを起こすのか?
結論からいうと、普通に使うと起こさないと思います。いろいろと試してみたのですが、SingleはObservableのメモリが開放される、onCompleted(Singleだとsuccess)とonErrorを必ず呼ぶので、何もしなくても自動開放されました。
サンプルコード1
import UIKit import RxSwift enum SampleError: Error { case unknown } class DetailViewController: UIViewController { let disposeBag = DisposeBag() var count: Int = 0 @IBAction func didTapStartButton() { print("start \(count)") createSingleLoad(count: count).subscribe(onSuccess: { result in print("onSuccess: message \(result)") }, onError: { error in print("onError \(error)") }).disposed(by: disposeBag) count += 1 } func createSingleLoad(count: Int) -> Single<String?> { return Single.create { event -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { event(.success("Success \(count)")) } return Disposables.create { print("disposed \(count)") } } } }
わざわざ dispossed(by: disposeBag) を使っていますが、使わなくても必ずdisposedされました。
サンプルコード2
@IBAction func didTapStartButton() { print("start \(count)") // これでもdipsosedされた _ = createSingleLoad(count: count).subscribe(onSuccess: { result in print("onSuccess: message \(result)") }, onError: { error in print("onError \(error)") }) count += 1 }
Singleでdispossed(by:)使うのは途中で処理をキャンセルしたいとき
呼んでしまったAPIのコール自体をキャンセルすることはできませんが、結果をみないようにするという意味ではキャンセル可能です。
サンプルコード1では、画面をpopすると処理が途中だったとしても、disposedすることができました。
これは以下の理由から。
– DetailViewControllerのインスタンスプロパティとして、disposeBagが定義されている
– DetailViewControllerがpopされたときにdisposeBagが開放
– ひもづいているsubscribeがキャンセル
あとは、例えばこのようにdisposeBagをOptionalにすれば処理中でもキャンセルできます。
キャンセルボタンを押すdidTapCancelButtonを呼んだときに、disposeBagをnilにして開放しています。
ただし、この場合はもしstartButtonを連打すると、処理途中のものもキャンセルされてしまいます。
反対にサンプルコード1の場合は連続でスタートボタンを押しても同時にSingleを実行することができました。
サンプルコード3
class DetailViewController: UIViewController { var disposeBag: DisposeBag? var count: Int = 0 @IBAction func didTapStartButton() { print("start \(count)") let bag = DisposeBag() disposeBag = bag createSingleLoad(count: count).subscribe(onSuccess: { result in print("onSuccess: message \(result)") }, onError: { error in print("onError \(error)") }).disposed(by: bag) count += 1 } @IBAction func didTapCancelButton() { disposeBag = nil } }
Observableでメモリリークをおこしてみよう。その1
次のコードはObservableを使っています。ただし、Observableの中でonCompletedやonErrorを呼んでいません。
また、disposed(by:)を使っていません。
この状態で画面をpopすると素敵にメモリリークします!
サンプルコード4
class DetailViewController: UIViewController { let disposeBag = DisposeBag() var count: Int = 0 @IBAction func didTapStartButton() { print("start \(count)") createObservableLoad(count: count).subscribe { [weak self] event in guard let self = self else { return } switch event { case .next(let text): print("text desu \(text)") case .error(let error): print("error \(error)") case .completed: print("completed!") } } count += 1 } func createObservableLoad(count: Int) -> Observable<String?> { return Observable.create { event -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { event.onNext("next dayo") event.onNext("next dayo2 count \(count)") } return Disposables.create { print("disposed \(count)") } } } }
最後にdisposed(by:)を使えばメモリリークしません
サンプルコード5
@IBAction func didTapStartButton() { print("start \(count)") createObservableLoad(count: count).subscribe { [weak self] event in guard let self = self else { return } switch event { case .next(let text): print("text desu \(text)") case .error(let error): print("error \(error)") case .completed: print("completed!") } }.disposed(by: disposeBag) count += 1 }
Observableでメモリリークをおこしてみよう。その2
循環参照を使ってメモリリークをおこしてみましょう!
クロージャーの中でそのままselfを使っているので、disposed(by:)を使っていてもメモリリークがおきました!
サンプルコード6
class DetailViewController: UIViewController { let disposeBag = DisposeBag() var count: Int = 0 var name: String = "Sample Name" @IBOutlet weak var startButton: UIButton! @IBAction func didTapStartButton() { print("start \(count)") createObservableLoad(count: count).subscribe { event in switch event { case .next(let text): print("text desu \(text)") //ここ self.startButton.setTitle("start2", for: .normal) case .error(let error): print("error \(error)") case .completed: print("completed!") } }.disposed(by: disposeBag) count += 1 } }
weak selfで、循環参照を解消しましょう!
サンプルコード7
@IBAction func didTapStartButton() { print("start \(count)") createObservableLoad(count: count).subscribe { [weak self] event in guard let self = self else { return } switch event { case .next(let text): print("text desu \(text)") self.startButton.setTitle("start2", for: .normal) case .error(let error): print("error \(error)") case .completed: print("completed!") } }.disposed(by: disposeBag) count += 1 }
ちなみに、Singleで循環参照を起こした場合、なんとメモリリークはおきませんでした。
これは、Singleが必ずonCompletedで破棄されるから(循環参照してても!)なのではないか?と思いますが、詳しいことはわかりませんです、、。
シングルトンでためす
こんなクラスを作ってみました
サンプルコード8
import Foundation import RxSwift class SampleService { static private(set) var shared: SampleService = SampleService() let disposeBag = DisposeBag() func startSingle(count: Int) { createSingleLoad(count: count).subscribe(onSuccess: { result in print("onSuccess \(result)") }, onError: { error in print("onError \(error)") }).disposed(by: disposeBag) } func startObservable(count: Int) { createObservableLoad(count: count).subscribe { event in switch event { case .next(let text): print("text desu \(text)") case .error(let error): print("error \(error)") case .completed: print("completed!") } }.disposed(by: disposeBag) } func createSingleLoad(count: Int) -> Single<String?> { return Single.create { event -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { event(.success("Success \(count)")) } return Disposables.create { print("disposed \(count)") } } } func createObservableLoad(count: Int) -> Observable<String?> { return Observable.create { event -> Disposable in DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { event.onNext("next dayo") event.onNext("next dayo2 count \(count)") // onCompleteさえ呼べば開放。呼ばないといつまでたっても開放されない event.onCompleted() } return Disposables.create { print("disposed \(count)") } } } }
使ってみる
サンプルコード9
SampleService.shared.startObservable(count: count) もしくは SampleService.shared.startSingle(count: count)
結論からいいますと、シングルトンの中にletのdisposeBagを使って、disposed(by:)するとメモリリークがおきました。
SingleやObservable自体は開放されているのですが、いつまでもsubscribeされているのではないか?と思います。
なので、disposed(by:)をしないようにすると、メモリが開放されました。
サンプルコード10
func startSingle(count: Int) { _ = createSingleLoad(count: count).subscribe(onSuccess: { result in print("onSuccess \(result)") }, onError: { error in print("onError \(error)") }) } func startObservable(count: Int) { _ = createObservableLoad(count: count).subscribe { event in switch event { case .next(let text): print("text desu \(text)") case .error(let error): print("error \(error)") case .completed: print("completed!") } } }
シングルトンでRxSwift使うときのまとめ
– SingleでないObservableなどの場合は、必ずonCompletedかonErrorをObservableの中から呼ぶこと(シングルトンのときだけ気をつける)
– クロージャーの中で循環参照をおこさないようにする
– disposed(by:)を呼ばない(シングルトンのときだけ気をつける)
– もしdisposed(by:)を使いたい場合は、disposeBagをOptionalにして、毎回開放すること
– メモリリークをおこす原因は、Observable側と、Subscribeする方のObserve側がある
ソースコード(コメントアウトばっかりで汚い!)
RxSample2
余談: 結局disposeBagって何なの?
ここに書いてありました。
Memory management in RxSwift – DisposeBag
– ObservableをsubscribeするとDisposableが返る
– そのときDisposable -> Observableと、Observable -> Disposableへの循環参照の状態になる(メモリリーク!)
– その参照を断ち切るのがObservableのdispose()
– dispose()はObservable自身がcompletedとerrorを発行したときに自動で呼ぶ
– もしObservableがcompletedとerrorを発行しなかったら、本来はsubscribeしているObserveがdeinitのタイミングでdisposeを手動で呼ばないといけない
– そこでdisposeBagの登場
– disposeBagにDisposableを全部登録しておけば、deinitのタイミングで自動で中に入っている複数のDisposableをdisposeしてくれる
このことからシングルトンでSingleを使っているときに、disposed(by:)を呼ばなくてよい理由がわかりました。
つまり、Observable自身がcompletedとerrorを発行したときにすでにdisposeを呼んでいるからです。
何らかの理由でSingleを任意のタイミングでキャンセルしたい場合はdisposed(by:)で管理する必要がありますが、そうでない場合はほっておいてもdisposeされるので何もしなくて良いということになります。
また、disposed(by:)したときのdisposeBagが残っている場合は、subscribe自体は残り続けます。(メモリリークというか、保持し続けるというか)
シングルトンでない、通常の場合
1. disposeBagが開放
2. このとき、もしObservableがcompleteとerrorを呼んでなくて、Observable自身でObservableを開放していなければ、disposeBagからdisposeする
3. subscribeもキャンセル。その分のメモリ開放
4. disposeBagを開放するのは、letのインスタンスプロパティとして保持している場合deinitのタイミング。またdisposeBagをoptionalにしておけば(DisposeBag?型)、任意のタイミングで開放できる
シングルトンでdisposed(by:)した場合
– deinitが実行されないためdisposeBagはoptionalにしない限り開放されない
– そのためsubscribeされ続ける(Observableが開放されていても)
■ 自作iPhoneアプリ 好評発売中!
・フォルメモ - シンプルなフォルダつきメモ帳
・ジッピー電卓 - 消費税や割引もサクサク計算!
■ LINEスタンプ作りました!
毎日使える。とぼけたウサギ