如何避免此 SwiftUI + Combine Timer Publisher 引用循环/内存泄漏?
How can I avoid this SwiftUI + Combine Timer Publisher reference cycle / memory leak?
我有以下 SwiftUI 视图,其中包含一个在五秒后消失的子视图。淡入淡出是通过接收 Combine TimePublisher 的结果触发的,但是更改 sink
发布者的接收器块中 showRedView
的值会导致内存泄漏。
import Combine
import SwiftUI
struct ContentView: View {
@State var showRedView = true
@State var subscriptions: Set<AnyCancellable> = []
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onAppear {
fadeRedView()
}
}
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.sink { _ in
withAnimation {
showRedView = false
}
}
.store(in: &subscriptions)
}
}
我认为这是通过 AnyCancellable
集合在幕后以某种方式进行管理的。我对 SwiftUI 和 Combine 比较陌生,所以我肯定是在这里搞砸了一些事情或者没有正确考虑它。避免这种泄漏的最佳方法是什么?
编辑:添加一些显示泄漏的图片。
视图应该被认为是描述视图的结构,以及它如何对数据做出反应。它们应该很小,single-purpose,easy-to-init 结构。他们不应该拥有自己的 life-cycles 实例(比如保持发布者订阅)——那些属于视图模型。
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
.prefix(1)
.map { _ in }
.eraseToAnyPublisher()
}
}
并使用 .onReceive
对视图中发布的事件作出反应:
struct ContentView: View {
@State var showRedView = true
@ObservedObject vm = ViewModel()
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onReceive(self.vm.pub, perform: {
withAnimation {
self.showRedView = false
}
})
}
}
因此,似乎通过上述安排,TimerPublisher
和 prefix
发布者链导致了泄漏。它也不是适合您的用例的发布者。
以下实现了相同的结果,没有泄漏:
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Just(())
.delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
我的猜测是你在泄漏,因为你在 subscriptions
中存储了一个 AnyCancellable
而你从未删除它。
sink
运算符创建 AnyCancellable
。除非您将其存储在某个地方,否则订阅将被提前取消。但是如果我们直接使用 Subscribers.Sink
订阅者,而不是使用 sink
运算符,那么就没有 AnyCancellable
可以让我们管理了。
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: { _ in
withAnimation {
showRedView = false
}
}
))
}
但这仍然是矫枉过正。为此,您不需要 Combine。您可以直接安排活动:
func fadeRedView() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
withAnimation {
showRedView = false
}
}
}
我有以下 SwiftUI 视图,其中包含一个在五秒后消失的子视图。淡入淡出是通过接收 Combine TimePublisher 的结果触发的,但是更改 sink
发布者的接收器块中 showRedView
的值会导致内存泄漏。
import Combine
import SwiftUI
struct ContentView: View {
@State var showRedView = true
@State var subscriptions: Set<AnyCancellable> = []
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onAppear {
fadeRedView()
}
}
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.sink { _ in
withAnimation {
showRedView = false
}
}
.store(in: &subscriptions)
}
}
我认为这是通过 AnyCancellable
集合在幕后以某种方式进行管理的。我对 SwiftUI 和 Combine 比较陌生,所以我肯定是在这里搞砸了一些事情或者没有正确考虑它。避免这种泄漏的最佳方法是什么?
编辑:添加一些显示泄漏的图片。
视图应该被认为是描述视图的结构,以及它如何对数据做出反应。它们应该很小,single-purpose,easy-to-init 结构。他们不应该拥有自己的 life-cycles 实例(比如保持发布者订阅)——那些属于视图模型。
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
.prefix(1)
.map { _ in }
.eraseToAnyPublisher()
}
}
并使用 .onReceive
对视图中发布的事件作出反应:
struct ContentView: View {
@State var showRedView = true
@ObservedObject vm = ViewModel()
var body: some View {
ZStack {
if showRedView {
Color.red
.transition(.opacity)
}
Text("Hello, world!")
.padding()
}
.onReceive(self.vm.pub, perform: {
withAnimation {
self.showRedView = false
}
})
}
}
因此,似乎通过上述安排,TimerPublisher
和 prefix
发布者链导致了泄漏。它也不是适合您的用例的发布者。
以下实现了相同的结果,没有泄漏:
class ViewModel: ObservableObject {
var pub: AnyPublisher<Void, Never> {
Just(())
.delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
我的猜测是你在泄漏,因为你在 subscriptions
中存储了一个 AnyCancellable
而你从未删除它。
sink
运算符创建 AnyCancellable
。除非您将其存储在某个地方,否则订阅将被提前取消。但是如果我们直接使用 Subscribers.Sink
订阅者,而不是使用 sink
运算符,那么就没有 AnyCancellable
可以让我们管理了。
func fadeRedView() {
Timer.publish(every: 5.0, on: .main, in: .default)
.autoconnect()
.prefix(1)
.subscribe(Subscribers.Sink(
receiveCompletion: { _ in },
receiveValue: { _ in
withAnimation {
showRedView = false
}
}
))
}
但这仍然是矫枉过正。为此,您不需要 Combine。您可以直接安排活动:
func fadeRedView() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
withAnimation {
showRedView = false
}
}
}