KVC 与 KVO 使用和原理解析

KVC

什么是 KVC

1.KVC 是 Key-Value-Coding 的简称。

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

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

KVC 主要方法

KVC 定义了一种按名称访问对象属性的机制,支持这种访问的主要方法是:

  • 设置值
// value的值为OC对象,如果是基本数据类型要包装成NSNumber
- (void)setValue:(id)value forKey:(NSString *)key;

// keyPath键路径,类型为xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

// 它的默认实现是抛出异常,可以重写这个函数做错误处理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
  • 获取值
- (id)valueForKey:(NSString *)key;

- (id)valueForKeyPath:(NSString *)keyPath;

// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;

NSKeyValueCoding 类别中还有其他的一些方法:

// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
+ (BOOL)accessInstanceVariablesDirectly;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

// KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;

举个栗子:

@interface Teacher : NSObject
{
    @private
    int _age;
}

@property (nonatomic, strong, readonly) NSString *name;

@property (nonatomic, assign, getter = isMale) BOOL male;

- (void)log;

@end

这个类有私有 private 变量和只读 readonly 变量,如果用一般的 setter 和 getter,在类外部是不能访问到私有变量的,不能设值给只读变量,那是不是就拿它没办法了呢?
然而 KVC 可以做到,就是这么神奇。

Teacher *teacher = [Teacher new];

[teacher log];

// 设置 readonly value
[teacher setValue:@"Jack" forKey:@"name"];
// teacher.name = @"Jack";

// 设置 private value
[teacher setValue:@24 forKey:@"age"];
// teacher.age = 24;

[teacher setValue:@1 forKey:@"male"];

[teacher log];

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

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

NSLog(@"male: %d", [[teacher valueForKey:@"isMale"] boolValue]);

KVC 实现细节

- (void)setValue:(id)value forKey:(NSString *)key;
  • 1 首先搜索 setter 方法,有就直接赋值。
  • 2 如果上面的 setter 方法没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
1)返回 NO,则执行`setValue:forUNdefinedKey:`
2)返回 YES,则按`_<key>,_<isKey>,<key>,<isKey>`的顺序搜索成员名。
  • 3 还没有找到的话,就调用setValue:forUndefinedKey:
- (id)valueForKey:(NSString *)key;
  • 首先查找 getter 方法,找到直接调用。如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
  • 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
1)返回 NO,则执行valueForUNdefinedKey:
2)返回 YES,则按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员名。
  • 还没有找到的话,调用valueForUndefinedKey:

修改私有属性

  • 1.修改 TextField 的 placeholder:
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];   

[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];
  • 2.修改 UIPageControl 的图片:
[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];

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

KVC 总结

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

优点:

1.不需要通过 setter、getter 方法去访问对象的属性,可以访问对象的私有属性。
2.可以轻松处理集合类(NSArray)。

缺点:

1.一旦使用 KVC 你的编译器无法检查出错误,即不会对设置的键、键值路径进行错误检查。
2.执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
3.使用 KVC 会破坏类的封装性。

KVO

1.KVO 是 Key-Value-Observing 的简称。

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

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

KVO 的原理

1.当一个 object(对象) 有观察者时候,动态创建这个 object(对象) 的类的子类 

2.对于每个被观察的 property(属性),重写其 setter 方法 

3.在重写的 setter 方法中调用 -willChangeValueForKey: 和 -didChangeValueForKey: 通知观察者 

4.当一个 property(属性) 没有观察者时,删除重写的方法

5.当没有 observer(观察者) 观察任何一个 property(属性) 时,删除动态创建的子类

举个栗子:

_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"];
}

KVO 实现原理

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

派生类 NSKVONotifying_Person 剖析:

在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

子类 setter 方法剖析:

KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。
当改变发生后,didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更。
之后,observeValueForKey:ofObject:change:context: 也会被调用。
重写观察属性的 setter 方法这种方式是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

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

总结:

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

1.系统会动态创建一个继承于 Person 的 NSKVONotifying_Person
2.person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法
3.重写NSKVONotifying_Person的setter方法:[super setName:newName]
4.通知观察者告诉属性改变。

KVO 应用

监听 ScrollView 的 contentOffSet 属性,采取相应的措施:

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

KVO 总结

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

优点:

1.提供一个简单的方法来实现两个对象的同步。
2.能够提供观察的属性的新值和旧值。
3.每一次属性值改变都是自动发送通知,不需要开发者手动实现。
4.用 keypath 来观察属性,因此也可以观察嵌套对象。

缺点:

1.观察的属性必须使用字符串来定义,因此编译器不会出现警告和检查
2.只能重写回调方法来后去通知,不能自定义 selector。当观察多个对象的属性时就要写"if"语句,来判断当前的回调属于哪个对象的属性的回调。

拓展-->

1.KVC与KVO的不同?

KVC(键值编码),即Key-Value Coding,一个非正式的Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用Setter、Getter方法等显式的存取方式去访问。
KVO(键值监听),即Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了setter方法、或者使用了KVC赋值。

2.和notification(通知)的区别?

notification比KVO多了发送通知的一步。
两者都是一对多,但是对象之间直接的交互,notification明显得多,需要notificationCenter来做为中间交互。而KVO如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句“交由系统通知”,具体的可参照以上实现过程的剖析。
notification的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。

3.与delegate的不同?

和delegate一样,KVO和NSNotification的作用都是类与类之间的通信。但是与delegate不同的是:
这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而delegate 则需要通信的对象通过变量(代理)联系;
delegate一般是一对一,而这两个可以一对多。

4.涉及技术:

KVC/KVO实现的根本是Objective-C的动态性和runtime,以及访问器方法的实现

总结

对比其他的回调方式,KVO机制的运用的实现,更多的由系统支持,相比notification、delegate等更简洁些,并且能够提供观察属性的最新值以及原始值;但是相应的在创建子类、重写方法等等方面的内存消耗是很巨大的。所以对于两个类之间的通信,我们可以根据实际开发的环境采用不同的方法,使得开发的项目更加简洁实用。 

另外需要注意的是,由于这种继承方式的注入是在运行时而不是编译时实现的,如果给定的实例没有观察者,那么KVO不会有任何开销,因为此时根本就没有KVO代码存在。但是即使没有观察者,委托和NSNotification还是得工作,这也是KVO此处零开销观察的优势。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343