KVC(Key-value coding)
KVC是一种基于NSKeyValueCoding非正式协议的机制,能让我们直接使用一个或一串字符串标识符去访问、操作类的属性。KVO 就是基于 KVC 实现的关键技术之一。
KVC基本使用
KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//keyPath为属性的路径 比如xx.xx
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//获取属性值时,如果属性不存在则执行该方法;
默认实现方式为抛出NSUnknownKeyException异常,可重写这个函数做错误处理
- (nullable id)valueForUndefinedKey:(NSString *)key;
//设置属性值,如果属性不存在则执行该方法,默认实现方式为抛出NSUnknownKeyException异常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 对非类对象属性设置nil时调用,默认抛出异常。
-(void)setNilValueForKey:(NSString *)key
KVC执行机制
setValue:forKey:搜索方式
1、首先搜索setKey:方法。(key指成员变量名,首字母大写)
2、如果上面的setter方法没找到,KVC机制会检查accessInstanceVariablesDirectly
类方法有没有返回YES(默认返回YES)。如果返回YES,那么按 _key,_isKey,key,iskey的顺序搜索成员变量并赋值;如果返回NO,那么在这一步KVC会执行setValue:forUndefinedKey:
方法。(如果开发者想让这个类禁用KVC里,那么重写accessInstanceVariablesDirectly
类方法让其返回NO即可)
3、如果没有找到成员变量,执行setValue:forUnderfinedKey:
方法。valueForKey:的搜索方式
1、首先按getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL、int等值类型,会做NSNumber的转换。
2、如果上面的getter没找到,查找countOfKey、objectInKeyAtindex、KeyAtindexes格式的方法。如果countOfKey和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray所有方法的代理集合的NSArray消息方法。
3、还没找到,查找countOfKey、enumeratorOfKey、memberOfKey格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的代理集合。
4、还是没找到,如果类方法accessInstanceVariablesDirectly
返回YES。那么按 _key,_isKey,key,iskey的顺序搜索成员名。
5、再没找到,调用valueForUndefinedKey
方法。
KVC实现分析
KVC运用了isa-swizzing技术。isa-swizzing就是类型混合指针机制。KVC通过isa-swizzing实现其内部查找定位。isa指针指向维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针和其他数据。
每个类都有一张方法表,是一个hash表,值是函数指针IMP,SEL的名称就是查表时所用的键。
SEL数据类型:查找方法表时所用的键。定义成char*,实质上可以理解成int值。
IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
KVO(Key - Value - Observer)
KVO是Objective-C对观察者设计模式的一种实现,是一种基于NSKeyValueObserving非正式协议的机制,Cocoa通过这个协议为所有遵守协议的对象提供了一种自动化的属性观察能力。指定一个被观察对象(如A类),当对象中的某个属性发生变化的时候,对象就会接收到通知,并作出相应的处理。在 MVC 设计架构下的项目,KVO 机制很适合实现 mode 模型和 view 视图之间的通讯。
KVO实现原理
KVO 的实现依赖于 Objective-C 强大的 Runtime,Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。
- 创建子类
当观察某对象A时,系统会在运行期动态的创建一个名为:NSKVONotifying_A的派生类,该类继承自对象A的本类。 - 重写Setter方法
在派生类中重写基类中任何被观察属性的setter方法。派生类在被重写的 setter 方法中实现真正的通知机制。 - 修改isa指针
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个动态生成的派生类。
⚠️苹果为什么要用子类监听setter方法,而不用分类呢?原因当你用分类监听setter方法的时候,原来类中setter方法就不会走了,这样不好,所以苹果使用了子类监听setter方法。
KVO基本使用
1、注册观察者
通过addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keypath属性的变化事件。
options参数是一些配置选项,用来指明通知发出的时机和通知响应方法observeValueForKeyPath:ofObject:change:context:
的change字典中包含哪些值:
NSKeyValueObservingOptionNew:change字典中应该包含改变后的新值。
NSKeyValueObservingOptionOld:change字典中应该包含改变前的旧值。
NSKeyValueObservingOptionInitial:在注册观察者消息发出后立即发送通知一次,值改变的时候也会发送通知
NSKeyValueObservingOptionPrior:分别在值修改前后发送通知(即一次修改有两次触发)
⚠️在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,在适当的时候将观察者移除,否则会导致观察者被释放带来的Crash
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2、监听回调
在观察者中实现observeValueForKeyPath:ofObject:change:context:
方法,当keypath属性发生变化后,KVO会回调这个方法来通知观察者。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
3、移除观察者
当观察者不需要监听时,可以调用removeObserver:forKeyPath:
方法将观察者移除,注意在观察者消失之前移除否者会导致crash。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
KVO触发模式
自动监听
KVO默认自动监听。
使用 KVO 机制的前提是遵循 KVO 的属性设置方式来变更属性值。
例如是否执行了 setter 方法、或者是否使用了 KVC 赋值。
如果赋值没有通过 setter 方法或者 KVC,而是直接修改属性对应的成员变量,例如:仅调用 ```_name = @"newName",这时是不会触发 KVO 机制,更加不会调用回调方法的。
KVO无法监听对象的成员变量。
手动触发
1)重写automaticallyNotifiesObserversForKey
类方法修改触发模式,返回YES表示自动,返回NO表示手动,可根据key值定制属性的触发模式。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
2)在值改变前后分别调用:willChangeValueForKey:
和didChangeValueForKey:
方法.
⚠️KVO触不触发与你的值改不改变没有关系,与willChangeValueForKey:
和didChangeValueForKey:
方法有没有调用有关系。
KVO属性依赖
当一个属性与有限个属性关联时需要建立属性依赖。
比如:dog属性依赖于Dog对象下的name和age属性。
方法一:重写keyPathsForValuesAffectingValueForKey:
方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
//Dog下面有两个属性:name,age
keypaths = [[NSSet alloc] initWithObjects:@"_dog.name",@"_dog.age", nil];
}
return keypaths;
}
方法二:用以下的命名格式实现一个类方法keyPathsForValuesAffecting<Key>
,<Key>是属性的名字(第一个字母要大写)。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDog
{
return [NSSet setWithObjects:@"_dog.name",@"_dog.age", nil];
}
⚠️当你在category里增加了一个计算属性时,你不能重写keyPathsForValuesAffectingValueForKey:方法,因为你不能在category里重写方法.在这种情况下,你可以用keyPathsForValuesAffecting<Key>的类方法来实现这个机制。
比较
1.KVC 与 KVO 的不同?
KVC(键值编码),即 Key-Value Coding,基于NSKeyValueCoding非正式协议的机制,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。
KVO(键值监听),即 Key-Value Observing,基于NSKeyValueObserving非正式协议的机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
2.KVO和、NSNotification(通知)和delegate的区别?
两者都是观察者模式。
KVO是被观察者直接发送消息给观察者,是对象间的交互,而通知则是观察者和被观察者通过通知中心对象之间进行交互,即消息由被观察者发送到通知中心对象,再由中心对象发给观察者,两者之间并不进行直接的交互。
NSNotification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。
delegate 一般是一对一,而这两个可以一对多。
参考
KVC官方文档
KVO官方文档
iOS开发技巧系列---详解KVC
KVO、Delegate、Notification 区别及相关使用场景