Почему можно удалить наблюдателя из потока, отличного от его исходного в Swift? - PullRequest
0 голосов
/ 05 мая 2020

Почему можно удалить наблюдателя из потока, отличного от исходного в Swift?

Я думаю, что нам, возможно, придется копаться в исходном коде Swift, но мне это действительно интересно. Вот пример кода для демонстрации:

class ViewController: UIViewController {

    var counter = 0
    @objc dynamic var testValue: Bool = false
    override func viewDidLoad() {
        super.viewDidLoad()
        Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true, block: { [weak self] (timer) in
            guard let self = self else {
                print("return")
                return
            }
            self.testValue = !self.testValue
        })

        addObserver(self, forKeyPath: "testValue", options: .new, context: nil)

        Timer.scheduledTimer(withTimeInterval: 7.0, repeats: false, block: { (timer) in
            DispatchQueue.global(qos: .background).async {
                self.removeObserver(self, forKeyPath: "testValue")
            }
        })
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "testValue" {
            print("counter : \(counter)")
            counter += 1
        }
    }
}

1 Ответ

1 голос
/ 05 мая 2020

Нет никаких технических проблем с удалением наблюдателя из другого потока, но я бы не советовал этого делать. Наибольшую озабоченность вызывают условия гонки между освобождением наблюдателя и его отменой регистрации в KVO. Как , документация для removeObserver(_:forKeyPath:) говорит:

Обязательно вызовите этот метод (или removeObserver(_:forKeyPath:context:)) до того, как какой-либо объект, указанный в addObserver(_:forKeyPath:options:context:), будет освобожден.

Теперь в вашем случае оба ваших таймера строго ссылаются на наблюдателя (что само по себе является проблемой; вы должны использовать шаблон [weak self] в своих таймерах и при необходимости аннулировать их), но если вы исправите это, вы теперь введете гонку.

Если вы добавите своих наблюдателей в init и удалите их в deinit, это устранит любые условия гонки. (Также, если вы используете современный синтаксис KVO , это также устраняет эту гонку за освобождение / отмену регистрации.)


Также помните, что observeValue(forKeyPath:of:change:context:) вызывается в потоке, который обновил testValue свойство. Поэтому, если вас беспокоят проблемы с потоками, следует также учитывать безопасность потоков как testValue, так и counter.

В отсутствие какой-либо синхронизации (например, возможно, вы собираетесь предположить, что testValue никогда не будет обновляться из фонового потока, даже если вы планируете удалить наблюдателя из фонового потока), Я бы посоветовал сделать предположения, наследующие observeValue явным образом. Итак, предполагая, что вы добавили своего наблюдателя следующим образом:

addObserver(self, forKeyPath: #keyPath(testValue), options: .new, context: &observerContext)

Затем вы можете добавить это dispatchPrecondition, чтобы сделать предположение явным:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
    guard context == &observerContext else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }

    if keyPath == #keyPath(testValue) {
        dispatchPrecondition(condition: .onQueue(.main)) // given that `counter` is not synchronized, let’s warn developer if ever updated `testValue` on background thread
        print("counter: \(counter)")
        counter += 1
    }
}

Очевидно, вам это не нужно предварительное условие, если вы сделали counter потокобезопасным с помощью некоторой синхронизации.


Кроме того, мы, очевидно, не будем использовать KVO для наблюдения за свойством текущего класса (обычно мы просто использовали бы Swift-наблюдатель ), но я предполагаю, что вы упростили это в иллюстративных целях.

...