概念
KVO意思是键值观察,它是观察Objective-C和Swift中可用的程序状态变化的技术之一。
这个概念很简单:当我们有一个带有一些实例变量的对象时,KVO允许其他对象对任何这些实例变量的更改进行监视。
KVO是观察者模式的实际示例。使Objective-C(和Obj-C桥接的Swift)与众不同的原因是,您添加到类中的每个实例变量都可以通过KVO立即观察到! (此规则有一些例外,我将在文章中讨论它们)。
但是在大多数其他编程语言中,这种工具并不是开箱即用的-您通常需要在变量的设置器中编写其他代码,以将值更改通知观察者。
Swift已从Objective-C继承了KVO,因此,要全面了解,您需要了解KVO在Objective-C中的工作方式。
KVO in Objective-C
现在我们有一个名为Person
的类,其属性name
和age
@interface Person: NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
现在,此类的对象可以通过KVO传达属性的更改,且无需其他代码!
因此,我们唯一需要做的就是在另一个类中开始观察:
@implementation SomeOtherClass
- (void)observeChanges:(Person *)person {
[person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSNumber *ageNumber = change[NSKeyValueChangeNewKey];
NSInteger age = [ageNumber integerValue];
NSLog(@"New age is: %@", age);
}
}
@end
现在,每次Person
上的age
属性发生变化时,我们能看到New age is: ...
从观察者打印到日志上。
如您所见,KVO交互涉及两种方法。
第一个是addObserver:forKeyPath:options:context:
,可以在任何NSObject
上调用它,包括Person
。此方法将观察者附加到对象。
第二个是observeValueForKeyPath:ofObject:change:context:
这是NSObject
中的另一个标准方法,我们必须在观察者的类中覆盖它。此方法用于接收观察通知。
第三种方法removeObserver:forKeyPath:context:
允许您停止观察。如果观察到的对象的生命周期超过观察者,请务必取消订阅通知。因此,只需在观察者的dealloc方法中删除订阅即可。
现在,让我们讨论一下KVO中使用的方法的参数。
我们用于附加观察者的方法在NSObject中声明
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
-
observer是将接收更改通知的对象。通常,您在此参数中提供self,因为
addObserver:
是从自己的实例方法内部调用的。 -
keyPath是一个字符串参数,在最简单的情况下,它只是您要观察的属性的名称。如果属性引用了复杂的对象层次结构,则可以是用于挖掘该层次结构的一组属性名称:
"person.father.age"
-
options是一个枚举,可用于自定义随通知一起传递的信息以及应在何时发送的信息。可用选项为
NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
,它们分别控制是否包含新值和旧值。还有NSKeyValueObservingOptionInitial
用于在订阅后立即触发通知,还有NSKeyValueObservingOptionPrior
用于区分集合中的更改,例如在NSArray
中删除的插入。 - context是对任意类的对象的引用,这有助于在某些复杂的用例中(例如在使用CoreData时)识别预订。在大多数其他情况下,您只需在此处提供nil即可。
我们用于处理更新通知的方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
- keyPath与调用观察者时提供的字符串值相同。您可能会问为什么这里也提供它。原因是我们可能一次观察多个属性,因此可以使用此参数将一个属性的通知与另一个属性的通知区分开。
- object是观察对象。由于我们可以观察到多个对象的变化,因此该参数使我们能够确定谁的属性发生了变化。
-
change是字典,其中包含有关更改后的值的信息。基于我们在订阅时提供的
NSKeyValueObservingOptions
,该词典可能包含键NSKeyValueChangeNewKey
下的当前值,NSKeyValueChangeOldKey
的旧值以及观察集合中的更改时的"diff"信息:NSKeyValueChangeIndexIndexesKey
和NSKeyValueChangeKindKey
- ** context**是订阅时提供的参考。同样,用于正确的观察识别,在大多数情况下可以忽略
当KVO不起作用时
尽管KVO看起来像魔术,但背后没有任何非凡之处。实际上,您可以直接访问其内部,默认情况下它们是隐藏的。
诀窍是Objective-C如何生成属性的设置器。当你声明一个属性:
@property (nonatomic, assign) NSInteger age;
由Objective-C生成的事实设置器等效于以下内容:
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
而且,如果您显式定义了setter而不调用这些willChangeValueForKey
和didChangeValueForKey
- (void)setAge:(NSInteger)age {
_age = age;
}
…KVO将停止为该属性工作。
因此,基本上,这两种方法willChangeValueForKey
和didChangeValueForKey
允许KVO将更新发送给订阅者,并且开发人员可以通过省略setter的这些调用来屏蔽。
重要的是要了解由Objective-C合成的每个@property
都会添加一个带有_前缀的隐藏实例变量。
例如,@property NSInteger age;
生成名称为_age
的实例变量,该变量可以像该属性一样进行访问:
self.age = 25;
self._age = 25;
区别在于self.age = 25;
触发setter的setAge:
,而self._age = 25;
直接更改存储的变量。
摆脱KVO的另一种方法是首先不使用@property,而是将实例变量存储在该类的匿名类别中:
@interface Person () {
NSInteger _privateVariable;
}
@end
对于此类变量,Objective-C不会生成setter和getter,因此无法启用KVO。
Swift中的KVO
Swift从Objective-C继承了对KVO的支持,但是与后者不同,默认情况下,Swift类中禁用了KVO。
Swift中使用的Objective-C类保持启用KVO,但对于Swift类,我们需要将基类设置为NSObject并在变量中添加@objc动态属性:
class Person: NSObject {
@objc dynamic var age: Int
@objc dynamic var name: String
}
Swift中有两种用于键值观察的API:旧的API来自Objective-C,而新的API更加灵活,安全且对Swift友好。
让我们从新的API开始:
class PersonObserver {
var kvoToken: NSKeyValueObservation?
func observe(person: Person) {
kvoToken = person.observe(\.age, options: .new) { (person, change) in
guard let age = change.new else { return }
print("New age is: \(age)")
}
}
deinit {
kvoToken?.invalidate()
}
}
如您所见,新API在订阅开始的地方使用闭包回调传递更改通知。
这样更加方便和安全,因为我们不再需要检查keyPath
,object
或context
,在该闭包中,没有其他通知会发送,仅是我们已订阅的通知。
这里有一种管理观察生命周期的新方法-订阅操作将返回NSKeyValueObservation类型的token
,该token
必须存储在某个位置,例如,在观察者类的实例变量中。
稍后,我们可以对该token
调用invalidate()
以停止观察,就像上面的deinit
方法一样。
最终更改与keyPath有关。 String容易出错,因为在重命名变量时,编译器将无法告诉您keyPath现在导致无处可去。取而代之的是,此新API使用Swift的特殊类型作为keyPath,这使编译器可以验证路径是否有效。
options
参数具有与Objective-C中相同的选项集。如果需要提供多个选项,只需将它们捆绑在一个数组中即可:options: [.new, .old]
虽然保留了所有缺点,但也可以使用旧的API,因此建议您改用新的API。
这是旧的:
class PersonObserver: NSObject {
func observe(person: Person) {
person.addObserver(self, forKeyPath: "age",
options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == "age",
let age = change?[.newKey] {
print("New age is: \(age)")
}
}
}
旧的API要求观察者也必须是NSObject的子类。我们还需要验证keyPath
,object
和context
,因为其他通知也以这种方法传递,就像在Objective-C中一样。
KVO替代品
现代iOS开发中还有很多其他技术可以达到相同的目的:状态更改的传播。我不得不说,KVO是最不常用的一种,因为替代品在便利性和多功能性方面往往超过其。实际上,我写了一系列文章,涵盖了状态更改传播的所有可用工具,并详尽地描述了每种工具的利弊。以下是快速参考:
- delegate
- NotificationCenter
- KVO
- Closure
- Target-Action
- Responder Chain
- Promise
- Event
- Stream of values
该系列文章的最后一篇是最终指南,我在其中描述一种工具比另一种工具更适合的实际情况。
最后,随着Apple的Combine框架的发布,KVO现在没有机会保持其最初的知名度,但是了解其工作原理仍然很重要!
翻译来自:https://nalexn.github.io/kvo-guide-for-key-value-observing/#kvo_swift
网友评论