英文:
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("onFire")
?
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("Emitter.start.create.dispose")
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("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 (is not related directly to the question, just for your convenience)
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")]),
]
)
答案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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论