iOS开发|KVO初探

Key Value Observing

Posted by Jack on May 10, 2014

前言

KVO(Key Value Observing),是观察者模式Foundation中的实现。

原理

KVO原理简单介绍如下:

  1. 当一个object有观察者时,动态创建这个object的类的子类
  2. 对于每个被观察的property,重写其set方法
  3. 在重写的set方法中调用- willChangeValueForKey:- didChangeValueForKey:通知观察者
  4. 当一个property没有观察者时,删除重写的方法
  5. 当没有observer观察任何一个property时,删除动态创建的子类

验证

口说无凭,简单验证下:验证工程GitHub链接

Jack *jack = [[Jack alloc] init];
// breakpoint 1
[jack addObserver:self forKeyPath:kPropertyThatChanges options:NSKeyValueObservingOptionNew context:nil];
// breakpoint 2
jack.name = @"Jack";
[jack removeObserver:self forKeyPath:kPropertyThatChanges];
// breakpoint 3

三处断点打印结果如下:

// breakpoint 1
(lldb) po jack.class
Jack
(lldb) po object_getClass(jack)
Jack

// breakpoint 2
(lldb) po jack.class
Jack
(lldb) po object_getClass(jack)
NSKVONotifying_Jack

// breakpoint 3
(lldb) po jack.class
Jack
(lldb) po object_getClass(jack)
Jack

上面的打印结果说明,在Jack对象被观察时,framework使用runtime动态创建了一个Jack类的子类NSKVONotifying_Jack

而且为了隐藏这个行为,NSKVONotifying_Jack重写了- class方法返回了之前的类,就好像什么也没发生过一样

但是使用object_getClass()时就暴露了,因为这个方法返回的是这个对象的isa指针,这个指针指向的一定是这个对象的类对象


为了进一步了解类内部的结构,我们来偷窥一下这个动态类实现的方法,这里借助一个NSObject的扩展NSObject+DLIntrospection,它封装了打印一个类的方法、属性、协议等常用调试方法,一目了然。

@interface NSObject (DLIntrospection)
+ (NSArray *)classes;
+ (NSArray *)properties;
+ (NSArray *)instanceVariables;
+ (NSArray *)classMethods;
+ (NSArray *)instanceMethods;
+ (NSArray *)protocols;
+ (NSDictionary *)descriptionForProtocol:(Protocol *)proto;
+ (NSString *)parentClassHierarchy;
@end

三个断点打印结果如下:

// breakpoint 1
(lldb) po [object_getClass(jack) instanceMethods]
<__NSArrayI 0x7fd988f0fb40>(
- (void).cxx_destruct,
- (id)name,
- (void)setName:(id)arg0 
)

// breakpoint 2
(lldb) po [object_getClass(jack) instanceMethods]
<__NSArrayI 0x7fd988e0ef20>(
- (void)setName:(id)arg0 ,
- (class)class,
- (void)dealloc,
- (BOOL)_isKVOA
)

// breakpoint 3
(lldb) po [object_getClass(jack) instanceMethods]
<__NSArrayI 0x7fd988f0d890>(
- (void).cxx_destruct,
- (id)name,
- (void)setName:(id)arg0 
)

从breakpoint2可以看出,动态类重写了4个方法:

  1. - setName:最主要的重写方法,set值时调用通知函数
  2. - class:为了隐藏自己而重写的方法,返回原来类的class
  3. - dealloc:做清理犯罪现场工作
  4. - _isKVOA:这就是内部使用的标识了,判断这个类有没有被KVO动态生成子类

接下来验证一下KVO重写set方法后是否调用了- willChangeValueForKey:- didChangeValueForKey:

最直接的验证方式就是在Jack类中重写这两个方法:

- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [super didChangeValueForKey:key];
}

运行程序发现,被观察的property发生改变时,的确会调用这两个方法。


参考文章:《objc kvo简单探索》