> For the complete documentation index, see [llms.txt](https://coderiding.gitbook.io/dgjj/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://coderiding.gitbook.io/dgjj/hu-lian-wang-gai-nian/qian-duan/ios/jie-shao-kvo-he-kvc.md).

# 介绍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)
