这是使用通知中心按特定顺序执行任务以避免内存泄漏的正确方法吗?

is this a correct way to use Notification Center to execute tasks in a certain order avoiding memory leaks?

提问人:biggreentree 提问时间:4/8/2023 更新时间:4/9/2023 访问量:117

问:

我正在测试通知和完成处理程序的用法,以按特定顺序执行多个耗时的任务。一位同事告诉我要检查可能的内存泄漏。

我的问题是:

  • 我的 LongTasksClass:这是使用通知和关闭的正确方法吗?
  • 在 iOS 9 之后不删除观察者是否正确?
  • 应该更好地在其他地方推出单例吗?

在我的视图控制器中,我有:

private var longTaskInstance: LongTasksClass? = LongTasksClass()

并在按钮中:

longTaskInstance?.startOperation()

我的班级

class LongTasksClass {
    
    enum TestingEnum: String {
        case firstOperation
        case secondOperation
        case stop
    }
    
    //public
    let testNotification = NSNotification.Name("test")
    
    //private
    private var myStatus: TestingEnum = .firstOperation
    private let notificationCenter = NotificationCenter.default
    
    
    init() {
        
        notificationCenter.addObserver(forName: self.testNotification, object: nil, queue: nil) { [weak self] notification in
            guard let self = self else {return}
            print(notification.userInfo?["info", default: "N/D"] ?? "no userInfo")
            
            self.doWholeOperation() {
                //first calls code in comletion
                //then calls this
                print("code after last completion")
            }
        }
        
    }
    
    
    func startOperation() {
        notificationCenter.post(name: self.testNotification, object: nil, userInfo: ["info" : "info start operation"])
    }
    
    //MARK: - private section -
    
    private func doWholeOperation(_ completion: @escaping (()) -> () ) {
        
        switch myStatus {
        case .firstOperation:
            print("very start: \(Date())\n")
            firstTimeConsumingTask { [weak self] in
                guard let self = self else {return}
                print("end first: \(Date())\n")
                self.myStatus = .secondOperation
                self.notificationCenter.post(name: self.testNotification, object: nil, userInfo: ["info" : "info sent from first task"])
            }
        case .secondOperation:
            secondTimeConsumingTask {
                print("end second: \(Date())\n")
                self.myStatus = .stop
                self.notificationCenter.post(name: self.testNotification, object: nil, userInfo: ["info" : "info sent from second task"])
            }
        case .stop:
            myStatus = .firstOperation
            let myCodeForCompletion: () = print("very end")
            completion(myCodeForCompletion)
            return
        }
    }
    
    private func firstTimeConsumingTask(_ completion: @escaping () -> ()) {
        print("start first task: \(Date())")
        //simulating long task
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            print("inside first task: \(Date())")
            completion()
        }
    }
    
    private func secondTimeConsumingTask(_ completion: @escaping () -> ()) {
        print("start second task: \(Date())")
        //simulating long task
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            print("inside second task: \(Date())")
            completion()
        }
    }
    
    //should not be required aymore
    deinit {
        print("‼️ called deinit")
        NotificationCenter.default.removeObserver(self)
    }
    
}
Swift 关闭通知 中心

评论


答:

1赞 Rob Napier 4/9/2023 #1

必须注销由 创建的通知观察程序。从文档中addObserver(forName:object:queue:using)

返回值:充当观察者的不透明对象。通知中心会严格保留此返回值,直到您删除观察者注册。

您必须调用或在系统解除分配任何指定的对象之前调用。removeObserver(_:)removeObserver(_:name:object:)addObserver(forName:object:queue:using:)

这是在返回值(必须存储)上调用的。它没有被调用.的要点是它创建了一个不透明的对象观察者;它不会成为观察者。removeObserveraddObserverselfaddObserver(forName:object:queue:using)self

关于 iOS 9,您正在考虑 addObserver(_:selector:name:object:),它使第一个参数(通常)成为观察者,并且通常不需要取消注册:self

如果您的应用面向 iOS 9.0 及更高版本或 macOS 10.11 及更高版本,则无需取消注册使用此函数创建的观察者。如果您忘记或无法删除观察者,系统会在下次发布观察对象时进行清理。

与往常一样,请参阅文档。

你错了(正如你怀疑的那样)。您发出的调用不是必需的,但您缺少删除您所做的观察所需的调用。deinit

作为一般规则,基于选择器的 API 更容易正确使用。人们往往只是喜欢基于块的 API,即使它们不太方便。

在您的示例中,所需的代码为:

class LongTasksClass {
...
    var observer: NSObjectProtocol
...
   init() {
        observer = notificationCenter.addObserver(forName: self.testNotification, object: nil, queue: nil) { [weak self] notification in
            ...   
        }     
    }
    ...
    deinit {
        notificationCenter.removeObserver(observer)
    }

评论

0赞 biggreentree 4/9/2023
谢谢,但从你的回答开始,哪个是删除观察者的正确方法?如果不在课堂上,它应该在哪里?
0赞 biggreentree 4/10/2023
谢谢!所以最后我误解了我需要存储方法的结果并使用该结果来删除观察者。可能人们不会期望视图控制器符合 NSObjectProtocol。这就是为什么许多人使用 self 作为默认值的原因。这是一个非常重要的概念。所以你认为代码的其余部分是一个很好的解决方案?
0赞 Rob Napier 4/10/2023
视图控制器始终符合 NSObjectProtocol(因为 UIViewController 符合 NSObjectProtocol)。与形式一起使用是不可能的。它不采用“observer”参数。块形式创建一个观察者对象并返回它。然后,您必须处理该对象。的选择器形式几乎总是作为观察者。selfaddObserveraddObserveraddObserverself
0赞 Rob Napier 4/10/2023
至于其余的代码,如果我想按顺序运行三个操作,我会使用串行 GCD 队列、OperationQueue 或完成块(或者在现代 Swift 中,使用 async/await 的结构化并发),而不是通知。如果你的代码依赖于发布通知时的观察者,那么它可能是错误的。通知的要点是它们与观察者的数量无关。发帖人应该对任何数量(包括 0)的观察者感到满意。事实上,您的通知是由发布它的同一函数处理的,这是一个好兆头,它过于复杂。