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此处零开销观察的优势。