ReactiveX / RxSwift

Reactive Programming in Swift
MIT License
24.33k stars 4.17k forks source link

Usage of `take(1).disposed(by: disposeBag)` isn't freed resources correctly #1422

Closed RafaelPlantard closed 6 years ago

RafaelPlantard commented 6 years ago

Short description of the issue:

When I have a single Observable value like that:

// UIViewController scope
let disposeBag = DisposeBag()

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    fetchHome()
}

func fetchHome() {
    Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag)
}

In the console I have found:

Resources total: 462
2017-09-25 09:39:35.329: HomeOnStoreViewController.swift:64 (fetchHome()) -> subscribed
2017-09-25 09:39:35.329: HomeOnStoreViewController.swift:64 (fetchHome()) -> Event next(1)
2017-09-25 09:39:35.330: HomeOnStoreViewController.swift:64 (fetchHome()) -> Event completed
2017-09-25 09:39:35.330: HomeOnStoreViewController.swift:64 (fetchHome()) -> isDisposed
Resources total: 463
Resources total: 463
Resources total: 463
Resources total: 463

The unique resource created by that is kept in memory.

And always the user enter in this view a new resource is created and the previous is kept in memory.

Expected outcome:

When I use .disposed(by: disposeBag) in a sequence of a unique take(1) value, I hope that the resource will be free after next event and completed, like that:

Resources total: 462
2017-09-25 09:39:35.329: HomeOnStoreViewController.swift:64 (fetchHome()) -> subscribed
2017-09-25 09:39:35.329: HomeOnStoreViewController.swift:64 (fetchHome()) -> Event next(1)
2017-09-25 09:39:35.330: HomeOnStoreViewController.swift:64 (fetchHome()) -> Event completed
2017-09-25 09:39:35.330: HomeOnStoreViewController.swift:64 (fetchHome()) -> isDisposed
Resources total: 462
Resources total: 462
Resources total: 462
Resources total: 462

What actually happens:

The resource never isn't free after disposed(by:)

Self contained code example that reproduces the issue:

let disposeBag = DisposeBag() // global scope
// ... any bunch of code

Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag) // in func scope

But when I remove disposed(by: disposeBag), just using, take(1), the resource is free:

_ = Observable<Int>.just(1).debug().take(1).subscribe()

or:

_ = Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: DisposeBag())

or:

let disposeBag = DisposeBag()
Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag)

RxSwift/RxCocoa/RxBlocking/RxTest version/commit RxBlocking: 4f4bd5732e8b952e54ae8a57739bcfb645de12bc: 3.6.1 RxCocoa: 84a08739ab186248c7f31ce4ee92d6f8a947d690: 3.6.1 RxTest: 1d00348a7848c91ab5bf5d197d2378aa6b2bb356: 3.6.1 RxSwift: f9de85ea20cd2f7716ee5409fc13523dc638e4e4: 3.6.1

Platform/Environment

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

Xcode version:

  Xcode version goes here: Xcode 9

:warning: Fields below are optional for general issues or in case those questions aren't related to your issue, but filling them out will increase the chances of getting your issue resolved. :warning:

Installation method:

I have multiple versions of Xcode installed: (so we can know if this is a potential cause of your issue)

Level of RxSwift knowledge: (this is so we can understand your level of knowledge and formulate the response in an appropriate manner)

kzaher commented 6 years ago

Hi @RafaelPlantard ,

when I do this

// UIViewController scope
let disposeBag = DisposeBag()

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    fetchHome()
}

func fetchHome() {
    Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag)
}

The resource is freed correctly.

You can apply this patch to this project and check it out.

diff --git a/RxExample/RxExample/Examples/APIWrappers/APIWrappersViewController.swift b/RxExample/RxExample/Examples/APIWrappers/APIWrappersViewController.swift
index 366f866f..5c3859d3 100644
--- a/RxExample/RxExample/Examples/APIWrappers/APIWrappersViewController.swift
+++ b/RxExample/RxExample/Examples/APIWrappers/APIWrappersViewController.swift
@@ -58,6 +58,8 @@ class APIWrappersViewController: ViewController {
     override func viewDidLoad() {
         super.viewDidLoad()

+        Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag)
+
         datePicker.date = Date(timeIntervalSince1970: 0)

         // MARK: UIBarButtonItem

I've noticed that you say:

let disposeBag = DisposeBag() // global scope
// ... any bunch of code

Observable<Int>.just(1).debug().take(1).subscribe().disposed(by: disposeBag) // in func scope

What is global scope. If this is module, then this will obviously leak disposables because dispose bag will keep reference to disposables. If you mean view controller then this behaves ok as far as I can test this.

I need clarification or more info to be able to repro this.