Title不重要
  • 初衷
  • 教程
    • Aria2教程
    • Mac重装教程
    • Hexo教程
    • v2ray教程
    • 翻墙教程
    • Flutter教程
    • Cocoapods教程
  • 混合框架
    • 方案一:原生+H5
      • a、Cordova
      • b、Ionic
      • c、微信小程序
    • 方案二:原生+JavaScript
      • a、React Native
      • b、Weex
      • c、快应用
    • 方案三:原生+自绘
      • a、QT for mobile
      • b、Flutter
  • 开发语言
    • Flutter
      • Dart
    • iOS
      • 1、Swift
      • 2、Objective-C
    • Android
      • Java
    • 2、Python
    • 3、JavaScript
    • 4、PHP
    • 5、Go
  • 开发者
    • 开发者个人
      • iOS
    • 开发者团队
      • 滴滴出行团队
      • 携程团队
      • Yelp团队
      • 大众点评-美团点评团队
  • 工具
    • Mac
      • 重装Mac
    • Windows
  • 干货
    • iOS干货
      • 音视频
      • 动画
      • 直播
      • Xcode
      • iOS 网站论坛
      • iOS 开源项目
        • Swift
      • iOS 视图控件
    • 书籍
      • 教育类
    • 其他干货
      • 设计干货
      • 电影网站
      • Telegram资源
    • Java干货
    • Android干货
      • Android 开源项目
  • 集成平台
    • 第三方支付
      • 微信支付
    • 高德地图
      • AMapSearch库
  • SDKS
    • iOS
      • Foundation
        • NSURLSession
  • 互联网概念
    • 后端
      • 黑石物理服务器
      • Redis
      • 360 Pika项目
      • MapReduce
      • Apache Kylin
      • Gears
      • 集群框架Spark
      • ACM国际大学生程序设计竞赛
      • Presto
      • Apache Storm
      • Apache Flink
      • Apache HBase
      • Apache Mesos
      • Apache Hadoop
      • Apache Hive
      • Apache Cassandra
      • LAMP
      • MySQL
      • 概念:微服务
    • 前端
      • iOS
        • 怎么知道一个NSObject对象占用多少内存?
        • Token过期的处理方案
        • 无侵入埋点
        • 电量优化
        • 介绍多线程
        • 介绍消息传递和转发
        • 介绍Runloop
        • 介绍Runtime
        • 介绍深拷贝和浅拷贝
        • 介绍KVO和KVC
        • 自定义的类,如何实现copy
        • 介绍Autorelease对象什么时候释放?
        • 介绍App卡顿
        • 介绍NSCoding协议和NSCopying协议
        • 介绍内存泄漏和内存溢出
        • 介绍Weak和Assign的区别
        • 介绍Delegate为什么用Weak
      • Android
        • Android中startService和bindService的区别
        • Android与vue js互相调用
      • 原理:直播流程
  • 互联网大会
    • QCon全球软件开发大会
    • AICon全球人工智能与机器学习大会
    • ArchSummit全球架构师峰会
    • GMTC全球大前端技术大会
      • 2019 GMTC
    • WWDC全球苹果开发者大会
      • WWDC2019
        • SwiftUI
      • WWDC2017
        • Universal Links
      • WWDC2018
      • WWDC资源
    • 谷歌I/O大会
  • 生活的智慧
    • 权谋学
Powered by GitBook
On this page
  • 一、什么是kvc
  • 1.1 kvc基本语法
  • 1.2 kvc的工作原理
  • 1.3 kvc例子
  • 1.4 key和keypath的区别
  • 1.5 kvc访问属性和用点语法访问属性的区别
  • 二、kvo的本质
  • 2.1 kvo原理:
  • 2.2 kvo语法
  • 2.3 kvo观察例子
  • 2.4 kvo和线程
  • 2.5 kvo使用注意
  • 2.6 kvo底层原理剖析
  • 三、kvc和kvo的keyPath一定是属性么?

Was this helpful?

  1. 互联网概念
  2. 前端
  3. iOS

介绍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 支持实例变量

Previous介绍深拷贝和浅拷贝Next自定义的类,如何实现copy

Last updated 5 years ago

Was this helpful?

kvo 只能手动支持

手动设定实例变量的kvo实现监听