如何理解iOS的KVO和KVC?
第一部分:KVO可以实现什么功能?
1.1 KVO 本质
-
- KVO 全称Key-Value-Observing
-
- KVO 观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者;KVO 是一个观察者模式
-
- KVO 只能对属性【对象下面的属性 】做出反应,不会用来对方法或者动作做出反应。
-
- 每一次属性值改变都是自动发送通知,不需要开发者手动实现。
-
- 注意:任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
-
- 当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
1.2 KVO 例子
- 例子1:
_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"];
}
- 例子2:监听 ScrollView 的 contentOffSet 属性
[scrollview addObserver:self
forKeyPath:@“contentOffset
options:NSKeyValueObservingOptionNew
context:nil];
1.3 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"];
1.4 KVO与runtime
- 1.当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
- 2.被观察对象的 isa 指针会指向一个中间类,而不是原来真正的类。
第二部分:KVC可以实现什么功能?
2.1 KVC 本质
-
- KVC 全称NSKeyValueCoding,中文:键值编码
-
- 通过字符串的名字(key)来访问类属性;
-
- 不通过调用Setter、Getter方法访问;
-
- 在运行时动态的访问和修改对象的属性,不是在编译时确定;
-
KVC 缺点
-
- 执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
-
- 使用 KVC 会破坏类的封装性。
-
- 难排查错误
-
2.2 功能1:对集合操作
- 例子1:求最大值
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
- 例子2:有一个 Transaction 对象的数组,对象有属性 amount 的话;当我们调用 [a valueForKeyPath:@"@max.amount"] 的时候,它会在数组 a 的每个元素中调用 -valueForKey:@"amount" 然后返回最大的那个。
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
2.3 功能2:对私有变量{如readonly变量—如private变量}赋值
- 修改系统的一些类的私有属性,必须先要拿到属性的名字,一般都是下划线开头的属性名
-
- 需改私有属性-例子1:
@interface Teacher : NSObject
{
@private int _age;// 私有变量,一般外部不能改变,通过kvc可以改变,前提你知道这个私有变量的名字;
}
@property (nonatomic, strong, readonly) NSString *name;// 只读变量,一个外部不能直接赋值,通过kvc可以改变
@property (nonatomic, assign, getter = isMale) BOOL male;
- (void)log;
@end
- 使用kvc前: 用一般的 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]);
-
- 需改私有属性-例子2:修改 TextField 的 placeholder: 注意这里用了keypath
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];
-
- 需改私有属性-例子3:修改 UIPageControl 的图片:
[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];
[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"];
2.4 功能3:字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
2.4.1 kvc 语法覆盖
-
key 和keypath的区别?
-
key只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: "frame");
-
keypath: 除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: "layer.cornerRadius”)
-
-
举个例子说明问题1:
-
例子:比如person有个属性是address,address有个属性是town,现在我们如何通过person访问town属性?
-
答:如果通过key来访问
id address = [person valueForKey:@"address"];
id town = [address valueForKey:@"town”];
// 如果通过keypath来访问
id town = [person valueForKeyPath:@"address.town"];
2.4.2 语法(设置值,设置数据)
// 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;
2.4.3 语法 (获取值,获取数据)
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
2.4.4 其他语法
// 允许直接访问实例变量,默认返回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;
转载自:https://juejin.cn/post/6869667125959589895