1.KVO

1.1 属性依赖的机制

在objc.io讲解KVC和KVO的一篇文章中,举了一个体现属性依赖机制的例子,例子的逻辑结构如下图。完整代码见github

属性依赖机制

图解:

  • 该图表现了通过改变L、a、b的滑块,更新上图右侧view的背景颜色的实现原理。
  • L、a、b代表Lab色彩空间中颜色的三种影响因素。
1
+ (NSSet<NSString *>) keyPathsForValuesAffectingValueForKey: (NSString *)key;
  • key:受key path影响的key。
  • return:返回影响指定key的一组key path。
  • 使用:+ (NSSet<NSString *>) keyPathsForValuesAffecting<Key>

详解:

  • red的变化受L影响,green变化受L和a影响,blue变化受L和b影响,最终red、green和blue共同决定color属性,即view显示的背景颜色。
  • 通过实现对应的+keyPathsForValuesAffectingRedComponent/GreenComponent/BlueComponent/Color来指定各个属性之间的依赖关系。
  • 添加属性后,会在代码提示中自动生成如下名称的方法,选择一个就好了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (NSSet *)keyPathsForValuesAffectingRedComponent
{
return [NSSet setWithObject:@"lComponent"];
}
+ (NSSet *)keyPathsForValuesAffectingGreenComponent
{
return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];
}
+ (NSSet *)keyPathsForValuesAffectingBlueComponent
{
return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}
+ (NSSet *)keyPathsForValuesAffectingColor
{
return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

1.2 KVO

添加对LabColor的实例对象labColor的属性color的观察者,并添加相应的响应事件,更新view的backgroundColor。

1
2
3
4
5
6
7
8
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context

对于options的几个可选值:

  • 获取变化之前的值用NSKeyValueObservingOptionOld,获取变化之后的值用NSKeyValueObservingOptionNew,二者都取用按位或。
  • 在添加观察者之前就立即发送变化的通知用NSKeyValueObservingOptionInitial,可以通过这种一次性的通知确定被观察者某属性的初始值。
  • 在即将发生变化之前发送通知用NSKeyValueObservingOptionPrior(通常都是发生变化后发送通知)。
    建议设置Context,避免子类和父类观察同一对象的同一属性。

注意: KVO的add方法并不对观察对象、被观察对象和context持有强引用,所以要自行确保对于观察对象、被观察对象和context的强引用。

1.3 手动通知

  • 如何手动通知?

当我们需要override 属性的setter方法时,有时候需要添加一些自定义的控制,再进行赋值。这时需要关闭系统自动调用 -willChangeValueForKey:-didChangeValueForKey:的行为,改为手动调用这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
return NO;
}
- (void)setLComponent:(double)lComponent;
{
if (_lComponent == lComponent) {
return;
}
[self willChangeValueForKey:@"lComponent"];
_lComponent = lComponent;
[self didChangeValueForKey:@"lComponent"];
}
  • 何时需要手动通知?
    • 使用 -will|didChangeValueForKey:的正确方式是,当你没有通过KVO兼容的accessor或setter改变property时,这种情况下KVO机制是不会捕获到变化的,需要手动触发。
    • 只要触发setter,即使当前值没有改变,也会产生KVO的通知。当处于效率考虑、希望避免这种无用通知时可以考虑手动通知。

1.4 KVO与线程

通常来说,不推荐把KVO和多线程混合使用。因为KVO的行为是同步的,发生变化的线程和处理变化的线程应该是同一个线程。如果要使用多队列或是线程,我们不应该跨队列或是跨线程使用KVO。

1.5 KVO实现细节

自动的KVO是通过一种叫isa-swizzling的技术实现的。isa指针通常是指向对象的类,类的分发表中包含着指向该类实现的方法的一些指针。
当观察者被注册为要观察对象的某个属性时,被观察者对象isa指针就被改变了,它不再指向实际的类,而是指向一个中间类。因此isa指针并不能反映实例所属的真正类。如果想要获取实例对象的类,应该使用class方法。

(然而,在Mike Ash一篇谈论KVO在runtime层面如何实现的文章中谈到,当你第一次观察指定类的对象时,KVO会在runtime创建一个全新的、继承自你的class的子类。在这个全新的类中,它override了任何被观察key的set方法,然后把你的对象的isa指针转移,这样你的对象就成为了这个新类的实例。被override的方法正是通知观察者实现的本质。逻辑是这样的:对于key的改变必须走key的set方法。通过override它的set方法,就可以劫持它,并且在它被调用的时候发送通知给观察者。苹果公司实在是不想将这个机制暴露出来,因此,除了setter之外,那个动态的子类还override了-class方法,来向你返回原始的类。如果你不仔细研究的话,KVO改变的对象就像没有被观察一样。私以为,Mike说得更接近真相。毕竟,真相只有一个。:P)

1.6 KVO相关API

KVO API

2.KVC

2.1 不需要@property的KVC

  • 直接添加 -<key>-set<Key>: 方法;
  • 要正确处理nil,需要override -setNilValueForKey: 方法;
  • 还可以通过override如下方法来让一个类支持KVC,但是会影响性能。
1
2
- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
  • Foundation 框架支持直接访问实例变量。请小心的使用这个特性。你可以去查看 +accessInstanceVariablesDirectly 的文档。这个值默认是 YES 的时候,Foundation 会按照 _<key>, _is<Key>, <key>is<Key> 的顺序查找实例变量。

2.2 通过集合代理对象实现KVC

2.3 常见错误

  • KVO 旨在观察 关系 (relationship) 而不是集合。我们不能观察 NSArray,我们只能观察一个对象的属性——而这个属性有可能是 NSArray。相似地,观察 self 不是永远都生效的。而且这不是一个好的设计。

2.4 KVV(键值验证)

  • KVV 也是 KVC API 的一部分。这是一个用来验证属性值的 API,只是它光靠自己很难提供逻辑和功能。
  • 用 KVV 验证 model 类的值是 Cocoa 的惯例。
  • 需要在model中提供 -validate<Key>:error:方法。

2.5 KVC相关API

KVC API

2.6 -valueForKey: 实现过程

valueForKey:

Ref: