# 介绍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一定是属性么？**

* kvc 支持实例变量
* kvo 只能手动支持[手动设定实例变量的kvo实现监听](https://yq.aliyun.com/articles/30483)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://coderiding.gitbook.io/dgjj/hu-lian-wang-gai-nian/qian-duan/ios/jie-shao-kvo-he-kvc.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
