简体   繁体   English

如何避免此 SwiftUI + 组合定时器发布器参考周期/memory 泄漏?

[英]How can I avoid this SwiftUI + Combine Timer Publisher reference cycle / memory leak?

I have the following SwiftUI view which contains a subview that fades away after five seconds.我有以下 SwiftUI 视图,其中包含一个在五秒钟后消失的子视图。 The fade is triggered by receiving the result of a Combine TimePublisher, but changing the value of showRedView in the sink publisher's sink block is causing a memory leak.淡入淡出是通过接收 Combine TimePublisher 的结果来触发的,但是在sink发布者的 sink 块中更改showRedView的值会导致 memory 泄漏。

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)
    }
}

I thought this was somehow managed behind the scenes with the AnyCancellable collection.我认为这是通过AnyCancellable集合在幕后以某种方式管理的。 I'm relatively new to SwiftUI and Combine, so sure I'm either messing something up here or not thinking about it correctly.我对 SwiftUI 和 Combine 比较陌生,所以肯定我要么在这里搞砸了一些东西,要么没有正确考虑它。 What's the best way to avoid this leak?避免这种泄漏的最佳方法是什么?

Edit: Adding some pictures showing the leak.编辑:添加一些显示泄漏的图片。

内存泄漏图片 1

内存泄漏图片 2

Views should be thought of as describing the structure of the view, and how it reacts to data.视图应该被认为是描述视图的结构,以及它如何对数据做出反应。 They ought to be small, single-purpose, easy-to-init structures.它们应该是小型、单一用途、易于初始化的结构。 They shouldn't hold instances with their own life-cycles (like keeping publisher subscriptions) - those belong to the view model.他们不应该拥有具有自己生命周期的实例(例如保留发布者订阅)——这些实例属于视图 model。

class ViewModel: ObservableObject {
   var pub: AnyPublisher<Void, Never> {
        Timer.publish(every: 2.0, on: .main, in: .default).autoconnect()
            .prefix(1)
            .map { _ in }
            .eraseToAnyPublisher()
    } 
}

And use .onReceive to react to published events in the View:并使用.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
            }
        })
    }
}

So, it seems that with the above arrangement, the TimerPublisher with prefix publisher chain is causing the leak.因此,似乎通过上述安排,带有prefix发布者链的TimerPublisher导致了泄漏。 It's also not the right publisher to use for your use case.它也不是用于您的用例的正确发布者。

The following achieves the same result, without the leak:以下实现了相同的结果,没有泄漏:

class ViewModel: ObservableObject {
   var pub: AnyPublisher<Void, Never> {
        Just(())
           .delay(for: .seconds(2.0), scheduler: DispatchQueue.main)
           .eraseToAnyPublisher()
    } 
}

My guess is that you're leaking because you store an AnyCancellable in subscriptions and you never remove it.我的猜测是您正在泄漏,因为您将AnyCancellable存储在subscriptions中并且您从未将其删除。

The sink operator creates the AnyCancellable .接收sink操作员创建AnyCancellable Unless you store it somewhere, the subscription will be cancelled prematurely.除非您将其存储在某处,否则订阅将被提前取消。 But if we use the Subscribers.Sink subscriber directly, instead of using the sink operator, there will be no AnyCancellable for us to manage.但是如果我们直接使用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
                    }
                }
            ))
    }

But this is still overkill.但这仍然是矫枉过正。 You don't need Combine for this.为此,您不需要合并。 You can schedule the event directly:您可以直接安排活动:

    func fadeRedView() {
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
            withAnimation {
                showRedView = false
            }

        }
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM