何时注销 KVO Operation isFinished 观察

When to unregister KVO observation of Operation isFinished

在这个简单的代码 (Xcode 8.3) 中,我创建了一个 Operation 子类实例,注册了对其 isFinished 属性 的 KVO 观察,并通过将其添加到我的队列:

class MyOperation : Operation {
    override func main() {
        print("starting")
        print("finishing")
    }
}

class ViewController: UIViewController {
    let q = OperationQueue()
    override func viewDidLoad() {
        super.viewDidLoad()
        let op = MyOperation()
        op.addObserver(self, forKeyPath: #keyPath(MyOperation.isFinished), options: [], context: nil)
        self.q.addOperation(op)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        print("Observed \(keyPath)")
        if let op = object as? Operation {
            op.removeObserver(self, forKeyPath: #keyPath(MyOperation.isFinished))
        }
    }
}

如您所见,我有一个 observeValue(forKeyPath... 的实现,当然,我的计划是在那里调用 removeObserver(forKeyPath...

问题是我的应用程序因 "MyOperation was deallocated while key value observers were still registered with it" 而崩溃。我们打印 "starting" 和 "finishing" 但我们从不打印 "Observed";该操作在 之前不存在,我收到 KVO 通知。

这似乎是第 22 条军规。如果我不能通过观察 isFinished 来移除观察者,我应该什么时候做? [我可以通过将我自己的 KVO-observable 属性 添加到 MyOperation 来解决这个问题,我在 main 的末尾设置了它。但是我必须这样做的想法很奇怪;这不就是为什么 isFinished 是可观察的,所以我可以在这里做我想做的事吗?]

Xcode 8.2 上测试完全相同的给定代码片段后,它正常工作,控制台显示:

starting
finishing
Observed Optional("isFinished")

问题的原因似乎是在 Xcode 8.3 上测试它,可能是一个错误 - 或者它可能是一个新行为 -。但是,我建议将其报告为错误。

Apple 在 Swift 3.1 (source) 中更改了 #keyPath 行为。目前 #keyPath(isFinished) returns "finished",它曾经 return "isFinished",并且 那是一个错误 。这是解释,因为在使用 KVOOperation class.

时很容易混淆

当您为 KVO 通知注册一个对象时

textView.addObserver(self,
                     forKeyPath: #keyPath(UITextView.isEditable),
                     options: [.new, .old],
                     context: nil)

Foundation 为它 (textView) 提供了调用 willChangeValue(forKey:)didChangeValue(forKey:) (this is done via isa-swizzling) 的新 setter 实现。传递给这些方法的密钥不是 getter (isEditable),不是 setter(setEditable:),而是 属性 名称 (editable)。

@property(nonatomic,getter=isEditable) BOOL editable

这就是为什么它应该从 #keyPath(UITextView.isEditable) 收到 editable


尽管操作 class 定义了广告属性

@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;

它希望观察 isFinishedisExecuting 键的通知。

Upon completion or cancellation of its task, your concurrent operation object must generate KVO notifications for both the isExecuting and isFinished key paths to mark the final change of state for your operation

这迫使我们在发布这些通知时使用文字字符串。

恕我直言,这是多年前犯下的错误,如果不破坏现有代码,很难从中恢复。