Observable.create 捕获行为

huangapple go评论68阅读模式
英文:

Observable.create capture behaviour

问题

这个代码中的问题在于,当执行 printOnSelf("onFire") 时出现了 尝试读取已释放引用。在调用 subject!.startEmitter() 之后,应该有两个对 TimerInvalidator 的引用:第一个来自于 Controller,即 let invalidator = TimerInvalidator(),第二个来自于 Observable.create 闭包,该闭包捕获了局部变量 invalidator,因此仍然持有对它的引用。

Controller 被释放时,对 TimerInvalidator 的第一个引用应该丢失,但第二个引用仍然在 Observable.create 闭包中保留。但由于 Controller 已经被释放,DisposeBag 也应该同时被释放,并且应该释放创建的可观察序列。这应该会释放 Observable.create 闭包,从而删除对 TimerInvalidator 的第二个引用。这意味着 TimerInvalidator 也应该被释放并使定时器无效。

但实际情况是,定时器在 Controller 已经被释放很久之后才触发,导致在访问 self 时出现错误。我错过了什么?


当然,更合理的做法是不要在 Observable.create 内部访问 self,而是在创建方法之后使用 .do(onNext:) 闭包内部访问它。或者可以通过更改以下方式在序列被释放时使定时器无效:

return Disposables.create {
    print("Emitter.start.create.dispose")
    invalidator.timer?.invalidate()
}

或者可以通过将 TimerInvalidator 捕获为 unowned 来修复:

return Observable.create { [unowned self, unowned invalidator] event in
    ...
}

但我对这种方法不适用的原因和这些修复为什么起作用感兴趣。


以下代码显然只是一个我作为包运行的最小示例。

main.swift

import Foundation
import RxSwift
import RxCocoa

class TimerInvalidator {
    var timer: Timer?
    
    deinit {
        print("TimerInvalidator.deinit")
        timer?.invalidate()
    }
}

class Controller {
    let db = DisposeBag()
    let invalidator = TimerInvalidator()
    
    let emitter = Emitter()
    
    func startEmitter() {
        print("Controller.startEmitter")
        emitter.createSignal(with: invalidator)
            .emit() // Do stuff
            .disposed(by: db)
    }
    
    deinit {
        print("Controller.deinit")
    }
}
        
class Emitter {
    func printOnSelf(_ string: String) {
        // Represents some operation on self
        print("\(Self.self): " + string)
    }
    
    func createSignal(with invalidator: TimerInvalidator) -> Signal<String> {
        return Observable.create { [unowned self] event in
            printOnSelf("onCreated")
            invalidator.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [unowned self] timer in
                printOnSelf("onFire")
                event.onNext("fire")
                event.onCompleted()
            }
            return Disposables.create {
                print("Emitter.start.create.dispose")
            }
        }
        .asSignal(onErrorJustReturn: "error")
    }
    
    deinit {
       print("Emitter.deinit")
    }
}

var subject: Controller? = Controller()
subject!.startEmitter()

DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
    subject = nil
}

RunLoop.current.run(mode: .default, before: Date() + 10)

Package.swift(与问题无直接关系,仅供您参考)

import PackageDescription

let package = Package(
    name: "StackOverflow",
    products: [
        .executable(name: "StackOverflow", targets: ["StackOverflow"])
    ],
    dependencies: [
        .package(url: "https://github.com/ReactiveX/RxSwift", exact: "6.5.0")
    ],
    targets: [
        .target(
            name: "StackOverflow",
            dependencies: ["RxSwift", .product(name: "RxCocoa", package: "RxSwift")]),
    ]
)
英文:

Why does this result in an attempted read of an already deallocated reference when doing printOnSelf(&quot;onFire&quot;)?

After subject!.startEmitter() call, there should be two references to the TimerInvalidator: the first one is from the Controller as let invalidator = TimerInvalidator() and the second one is from the Observable.create closure which captures the local variable invalidator and therefore holds a reference to it.

When Controller gets deallocated, the first reference to the TimerInvalidator should be lost, while the second one is still captured inside the Observable.create closure. But because the Controller has been deallocated, the DisposeBag should also get deallocated at the same time and dispose the created observable sequence. That should deallocate the Observable.create closure and therefore remove the second reference to the TimerInvalidator. That should mean that the TimerInvalidator should also get deallocated and invalidate the timer.

But in reality, the timer fires long after the Controller has been freed, causing an error when accessing self. What am I missing?


Of course it would make much more sense not to access self inside the Observable.create, but to do it inside a .do(onNext:) closure after the create method. Or it could be fixed by invalidating the timer when the sequence is disposed by changing:

return Disposables.create {
    print(&quot;Emitter.start.create.dispose&quot;)
    invalidator.timer?.invalidate()
}

Or it could be fixed by capturing TimerInvalidator as unowned by changing:

return Observable.create { [unowned self, unowned invalidator] event in
    ...

But I am interested in the reason, why this approach is not fine. And also why those fixes work.


The following code is obviously just a minimal example, that I am running as a package.

main.swift

import Foundation
import RxSwift
import RxCocoa

class TimerInvalidator {
    var timer: Timer?
    
    deinit {
        print(&quot;TimerInvalidator.deinit&quot;)
        timer?.invalidate()
    }
}

class Controller {
    let db = DisposeBag()
    let invalidator = TimerInvalidator()
    
    let emitter = Emitter()
    
    func startEmitter() {
        print(&quot;Controller.startEmitter&quot;)
        emitter.createSignal(with: invalidator)
            .emit() // Do stuff
            .disposed(by: db)
    }
    
    deinit {
        print(&quot;Controller.deinit&quot;)
    }
}
        
class Emitter {
    func printOnSelf(_ string: String) {
        // Represents some operation on self
        print(&quot;\(Self.self): &quot; + string)
    }
    
    func createSignal(with invalidator: TimerInvalidator) -&gt; Signal&lt;String&gt; {
        return Observable.create { [unowned self] event in
            printOnSelf(&quot;onCreated&quot;)
            invalidator.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [unowned self] timer in
                printOnSelf(&quot;onFire&quot;)
                event.onNext(&quot;fire&quot;)
                event.onCompleted()
            }
            return Disposables.create {
                print(&quot;Emitter.start.create.dispose&quot;)
            }
        }
        .asSignal(onErrorJustReturn: &quot;error&quot;)
    }
    
    deinit {
       print(&quot;Emitter.deinit&quot;)
    }
}

var subject: Controller? = Controller()
subject!.startEmitter()

DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
    subject = nil
}

RunLoop.current.run(mode: .default, before: Date() + 10)

Package.swift (is not related directly to the question, just for your convenience)

import PackageDescription

let package = Package(
    name: &quot;StackOverflow&quot;,
    products: [
        .executable(name: &quot;StackOverflow&quot;, targets: [&quot;StackOverflow&quot;])
    ],
    dependencies: [
        .package(url: &quot;https://github.com/ReactiveX/RxSwift&quot;, exact: &quot;6.5.0&quot;)
    ],
    targets: [
        .target(
            name: &quot;StackOverflow&quot;,
            dependencies: [&quot;RxSwift&quot;, .product(name: &quot;RxCocoa&quot;, package: &quot;RxSwift&quot;)]),
    ]
)

答案1

得分: 2

以下是翻译好的部分:

"observable contract guarantees that the dispose() of the subscription will be called when the dispose bag's deinit is called. It makes no guarantees as to exactly when the memory associated with the Observable will be cleaned up. That's up to the underlying iOS VM which, if you look at the memory graph of the TimerInvalidator at the moment your app crashes, is still holding on to the Observable.

You probably noticed that at the time the app crashed, your TimerInvalidator's deinit had not yet been called. However, if you remove the offending line and let the app continue, the deinit does get called.

Keep in mind that the Rx system was designed to have determinable behavior no matter what the underlying memory model is. That's why the create function requires that you return a Disposable that correctly cleans up resources. By failing to do that, you are breaking the contract which leaves the behavior of the library undefined.

You said:
> ...it could be fixed by invalidating the timer when the sequence is disposed...

In fact, that is the only valid (as in following the contract) way to fix it."

英文:

The observable contract guarantees that the dispose() of the subscription will be called when the dispose bag's deinit is called. It makes no guarantees as to exactly when the memory associated with the Observable will be cleaned up. That's up to the underlying iOS VM which, if you look at the memory graph of the TimerInvalidator at the moment your app crashes, is still holding on to the Observable.

You probably noticed that at the time the app crashed, your TimerInvalidator's deinit had not yet been called. However, if you remove the offending line and let the app continue, the deinit does get called.

Keep in mind that the Rx system was designed to have determinable behavior no matter what the underlying memory model is. That's why the create function requires that you return a Disposable that correctly cleans up resources. By failing to do that, you are breaking the contract which leaves the behavior of the library undefined.

You said:
> ...it could be fixed by invalidating the timer when the sequence is disposed...

In fact, that is the only valid (as in following the contract) way to fix it.

huangapple
  • 本文由 发表于 2023年8月5日 06:29:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76839409.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定