介绍KVO和KVC

一、什么是kvc

  • kvc 是 Key-Value-Coding 的简称。

  • kvc 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。

  • 我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定,kvc 是 iOS 开发中的黑魔法之一。

1.1 kvc基本语法

获取值

valueForKey: 传入NSString属性的名字。 
valueForKeyPath: 属性的路径,xx.xx
valueForUndefinedKey: 默认实现是抛出异常,可重写这个函数做错误处理 

修改值

setValue:forKey: 
setValue:forKeyPath: 
setValue:forUnderfinedKey: 
setNilValueForKey: 对非类对象属性设置nil时调用,默认抛出异常

1.2 kvc的工作原理

  1. 直接找setter方法

  2. 找property Ivar,进行object_setIvar

  3. 找 _property Ivar,进行Ivar赋值

  4. setValue: forUndefinedKey

分析1: 假如我们调用 [[NSObject alloc] setValue:nil forKey:@"property"],其 kvc 调用如下所示:

  1. 去模型中查找有没有对应的 setter 方法:例如:setProperty 方法,有就直接调用这个 setter 方法给属性赋值;

  2. 如果找不到 setter 方法,接着就会去寻找有没有 property Ivar,如果有,就直接进行 void object_setIvar ( id obj, Ivar ivar, id value ) 赋值;

  3. 如果找不到 property 属性,接着又会去寻找 _property Ivar,如果有,直接进行 Ivar 赋值

  4. 如果都找不到会调用 setValue: forUndefinedKey:, 然后报出如下所示的崩溃信息。

1.3 kvc例子

用一般的 setter 和 getter,在类外部是不能访问到私有变量的,不能设值给只读变量 用kvc突破限制

Teacher *teacher = [Teacher new]; 
// name在类Teacher是只读,用kvc设置name值
// 设置 readonly value 
[teacher setValue:@"Jack" forKey:@"name"]; 

// age在类Teacher是私有变量,用kvc设置age值
// 设置 private value 
[teacher setValue:@24 forKey:@"age"]; 

// 获取 readonly value 
NSLog(@"name: %@", [teacher valueForKey:@"_name"]); 

// 获取 private value 
NSLog(@"age: %d", [[teacher valueForKey:@"_age"] intValue]); 

修改 TextField 的 placeholder的私有属性

[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];    
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"]; 

修改 UIPageControl 的图片私有属性

[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"]; 
[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"]; 

1.4 key和keypath的区别

  • key :只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: "frame");

  • keypath: 除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: "layer.cornerRadius")

1.5 kvc访问属性和用点语法访问属性的区别

  1. 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 kvc 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。

  2. 相比点语法用 kvc 方式 kvc 的效率会稍低一点,但是灵活{coderiding解析:灵活的意思是当你在编译前不确定你要访问什么属性,而要等到运行的时候,根据具体情况而言来访问,这时候kvc就有它的价值},可以在程序运行时决定访问哪些属性。

  3. 用 kvc 可以访问对象的私有成员变量。

二、kvo的本质

  1. kvo 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法

  2. kvo 是 Key-Value-Observing 的简称。

  3. kvo 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 kvo 会自动通知观察者。

  4. 更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。

  5. 每次对象的属性被改变后,那么监听者就会收到通知

  6. kvo 是通知的一种,还有的通知就是 NSNotificationCenter

  7. 在ObjC中要实现kvo则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的ObjC对象都可以使用kvo。

  8. 一个对象,可以被多个观察,所以每次销毁的时候就要移除观察

  9. kvo 是一个对象能观察另一个对象属性的值,kvo

  10. 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。kvo 只能对属性做出反应,不会用来对方法或者动作做出反应。

优点:

  • 提供一个简单的方法来实现两个对象的同步。

  • 能够提供观察的属性的新值和旧值。

  • 每一次属性值改变都是自动发送通知,不需要开发者手动实现。

  • 用 keypath 来观察属性,因此也可以观察嵌套对象。

缺点:

  • 观察的属性必须使用字符串来定义,因此编译器不会出现警告和检查

  • 只能重写回调方法来后去通知,不能自定义 selector。当观察多个对象的属性时就要写"if"语句,来判断当前的回调属于哪个对象的属性的回调。

2.1 kvo原理:

  • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

2.2 kvo语法

//【注册】1.注册观察者,实施监听
[self.person addObserver:self 
              forKeyPath:@"age" 
                 options:NSKeyValueObservingOptionNew 
                 context:nil]; 

//【观察】2.观察方法,回调方法,在这里处理属性发生的变化; 
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSString *,id> *)change 
                       context:(void *)context 

//【移除】3.移除观察者; 
[self removeObserver:self forKeyPath:@“age"]; 

2.3 kvo观察例子

_person = [[Person alloc] init]; 
    
/** 
 *  添加观察者 

 *  @param observer 观察者 
 *  @param keyPath  被观察的属性名称 
 *  @param options  观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项) 
 *  @param context  上下文,可以为nil。 
 */ 
[_person addObserver:self 
          forKeyPath:@"age" 
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 
             context:nil]; 

/** 
 *  kvo 回调方法 
 * 
 *  @param keyPath 被修改的属性 
 *  @param object  被修改的属性所属对象 
 *  @param change  属性改变情况(新旧值) 
 *  @param context context 传过来的值 
 */ 
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSString *,id> *)change 
                       context:(void *)context 
{ 
    NSLog(@"%@ 对象的%@属性改变了:%@",object,keyPath,change); 
} 

/** 
 *  移除观察者 
 */ 
- (void)dealloc 
{ 
    [self.person removeObserver:self forKeyPath:@"age"]; 
} 

监听 ScrollView 的 contentOffSet 属性

[scrollview addObserver:self 
             forKeyPath:@"contentOffset"                    
                options:NSKeyValueObservingOptionNew 
                context:nil]; 

kvo选择不监听某个属性

// 通知 key 的观察者    
+ ( BOOL ) automaticallyNotifiesObserversForKey: ( NSString * ) key{   
    // 不观察这个 key   
    if ([ key isEqualToString:@"link" ]) {   
        return NO ;    
    }   
    // 调用系统的方法    
    return [ super automaticallyNotifiesObserversForKey:key ];    
}  

kvo通过po查看被观察对象的所有观察信息

// 这会打印出有关谁观察谁之类的很多信息。
(lldb) po [observedObject observationInfo]

2.4 kvo和线程

  • 一个需要注意的地方是,kvo 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 kvo 通知。

  • 所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 kvo 通知。通常来说,我们不推荐把 kvo 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 kvo。

  • kvo 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),kvo 会保证下列两种情况的发生:

  • 首先,如果我们调用一个支持 kvo 的 setter 方法,如下所示:

self.exchangeRate = 2.345; 
  • kvo 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。

  • 其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRate 的 accessor 方法都会返回同样的值。

2.5 kvo使用注意

  • 首先,KVO 兼容是 API 的一部分。如果类的所有者不保证某个属性兼容 KVO,我们就不能保证 KVO 正常工作。苹果文档里有 KVO 兼容属性的文档。例如,NSProgress 类的大多数属性都是兼容 KVO 的。

  • 当做出改变以后,有些人试着放空的 -willChange 和 -didChange 方法来强制 KVO 的触发。KVO 通知虽然会生效,但是这样做破坏了有依赖于 NSKeyValueObservingOld 选项的观察者。详细来说,这影响了 KVO 对观察键路径 (key path) 的原生支持。KVO 在观察键路径 (key path) 时依赖于 NSKeyValueObservingOld 属性。

  • 我们也要指出有些集合是不能被观察的。KVO 旨在观察关系 (relationship) 而不是集合。我们不能观察 NSArray,我们只能观察一个对象的属性——而这个属性有可能是 NSArray。举例说,如果我们有一个 ContactList 对象,我们可以观察它的 contacts 属性。但是我们不能向要观察对象的 -addObserver:forKeyPath:... 传入一个 NSArray。

  • 相似地,观察 self 不是永远都生效的。而且这不是一个好的设计。

2.6 kvo底层原理剖析

/// 创建person对象,监听person的age属性
- (void)configurePesonClass {
    self.person = [[Person alloc] init];
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | 	NSKeyValueObservingOptionOld;
    [self.person addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}

剖析:

  1. self.person = [[Person alloc] init];

  2. 系统会动态创建一个继承于 Person 的 NSKVONotifying_Person

  3. person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法

  4. 重写NSKVONotifying_Person的setter方法:[super setName:newName]

  5. 通知观察者告诉属性改变。

派生类 NSKVONotifying_Person 剖析:

  • 在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。

  • 所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。

  • 因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

- (void)setName:(NSString *)newName 
{ 
    [self willChangeValueForKey:@"name"];    // KVO 在调用存取方法之前总调用 
    [super setValue:newName forKey:@"name"]; // 调用父类的存取方法 
    [self didChangeValueForKey:@"name"];     // KVO 在调用存取方法之后总调用 
} 

三、kvc和kvo的keyPath一定是属性么?

Last updated