如果我更改了一个属性的值,KVO会帮助我们检测这个值的变化,从而通知我们这个值改变了。典型的观察者模式。当然我想起在UNITY3D中,如果检视面板的数值发生改变对应的GameObject发生位移之类的。我曾经实现了一个inspector的编辑器类,值改变直接通知(其实是调用)对应的函数。在iOS这里 一切发生的那么自然。不需要额外实现 这是iOS的特性之一 名曰:KVO 。

前情提要

上一篇 KVC 讲到需要遵循的几条几本规则在 KVO 中同样适用。

  • 骆驼命名法,不能数字开头
  • 不能包含空格
  • 键必须是ASCII编码的
  • 使用默认的get/set

上一章 只要是针对 object 的元素查询,调用,筛选。本文主要是讲述 KVO 监听(观察者模式) 属性值的变更。

添加/移除 一个监听

addObserver 函数签名如下

  • addObserver:监听的接受脚本的类
  • context: 随便传入任何值都可以,最后取出来的时候需要强转
  • forKeyPath: 选择一个需要监听的属性
  • options: 可选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <Foundation/Foundation.h>
#import "Foo.h"
#import "Bar.h"

int main(int argc, const char * argv[]) {
Foo* foo = [[Foo alloc]init];
foo.bar = [[Bar alloc]init];
[foo.bar addObserver:foo forKeyPath:@"stringOnBar" options:0 context: (void*)s];

foo.bar.stringOnBar = @"test";

[foo.bar removeObserver:foo forKeyPath:@"stringOnBar"];


return 0;
}

控制台输出如下:

1
2
3
2017-03-03 15:17:22.085142 oc[52721:6279405] helloworld
2017-03-03 15:17:22.085371 oc[52721:6279405] Value Changed : stringOnBar
Program ended with exit code: 0

关于 options 可选项

上面的代码中 options 是个可选项,这里不需要处理所以填写0,详细的选项如下:

1
2
3
4
5
NSKeyValueObservingOptionNew     把更改之前的值提供给处理方法
NSKeyValueObservingOptionOld 把更改之后的值提供给处理方法
NSKeyValueObservingOptionInitial 把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
NSKeyValueObservingOptionPrior2次调用。在值改变之前和值改变之后。
0 不带任何参数进去

传递给监听的值在接受函数 ofObject:(id)object 可以获取到。

关于监听脚本

不用担心监听脚本过于复杂。其实是自动生成的 键入 observeValueForKeyPath 则会生成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{

NSString* content = (__bridge NSString*)context;

if (content) {
NSLog(@"%@",content);
}

if ([keyPath isEqualToString:@"stringOnBar"]) {
NSLog(@"Value Changed : stringOnBar");
return;
}
if ([keyPath isEqualToString:@"stringOnFoo"]) {
NSLog(@"Value Changed : stringOnFoo");
return;
}

[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

当然结构体里面的内容是我自己些的 😄

当类 dealloc 的时候需要手动移除监听,否则会报错 … 报错信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

2017-03-03 15:16:02.560251 oc[52593:6273946] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x1004065e0 of class Bar was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x100406b90> (
<NSKeyValueObservance 0x100407e80: Observer: 0x1004045e0, Key path: stringOnBar, Options: <New: NO, Old: NO, Prior: NO> Context: 0x1000030c0, Property: 0x100406ac0>
)'
*** First throw call stack:
(
0 CoreFoundation 0x00007fffa311fe7b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fffb7d09cad objc_exception_throw + 48
2 CoreFoundation 0x00007fffa319e99d +[NSException raise:format:] + 205
3 Foundation 0x00007fffa4aff6e4 NSKVODeallocate + 293
4 oc 0x000000010000278e -[Foo .cxx_destruct] + 78
5 libobjc.A.dylib 0x00007fffb7d05686 _ZL27object_cxxDestructFromClassP11objc_objectP10objc_class + 127
6 libobjc.A.dylib 0x00007fffb7cfe0c6 objc_destructInstance + 92
7 libobjc.A.dylib 0x00007fffb7cfe059 object_dispose + 22
8 oc 0x000000010000166e main + 398
9 libdyld.dylib 0x00007fffb85ed255 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

手动KVO

KVO不是万能的,有时候我们在某些条件下不希望接受到KVO的通知,需要重写如下方法:

1
2
3
4
5
6
7
NSInteger HP = 10;
+(BOOL)automaticallyNotifiesObserversOfStringOnFoo
{
if(HP>100)
return YES;
return NO;
}

这里有一个技巧输入 +(BOOL)automatically 之后自动列出可以重写的几个属性的通知。

手动触发KVO也是允许的,下面主要使用到了 willChangeValueForKey / didChangeValueForKey 这两个函数。即使禁止了自动通知也可以直接在get函数中触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSInteger HP = 11;

+(BOOL)automaticallyNotifiesObserversOfStringOnFoo
{
if(HP>100)
return YES;
return NO;
}

-(void)setStringOnFoo:(NSString *) inValue
{
[self willChangeValueForKey:@"stringOnFoo"];
stringOnFoo = inValue;
[self didChangeValueForKey:@"stringOnFoo"];
}