1、KVO
1、KVO:(Key-Value Observing)即键值观察,是一种观察者机制。当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。
2、实现原理
示意图
实现观察者之后调用
实现原理
KVO 是通过 isa-swizzling 技术实现的。在第一次为对象添加观察者时,iOS 系统会修改这个对象的 isa 指针指向一个全新的通过 Runtime 动态创建的子类。子类拥有自己的 set 方法实现,set 方法实现内部会顺序调用 willChangeValueForKey 方法、原来的 setter 方法实现、didChangeValueForKey 方法。didChangeValueForKey 方法内部又会调用监听器的 observeValueForKeyPath:ofObject:change:context:监听方法。在该子类中完成我们自己的操作后通过通知机制保证方法继续执行下去。
注意事项
1、KVO 观察的是 setter 方法。故可以用来观察属性,不能用来观察成员变量。
2、对象的 isa 指针重新指向了派生类(派生类的名字规则为 NSKVONotifying_原有类名)。
3、派生类重写了被观察属性的 setter 方法、class、dealloc、_isKVO 方法。
4、dealloc 方法中,移除 KVO 观察者之后,isa 指针由指向由派生类指向原有的类。
5、派生类在移除观察者之后并不会被立即移除,而是还一直存在内存中以备用。
3、实现步骤
1、注册观察者:addObserver:forKeyPath:options:context
2、实现观察者方法:observeValueForKeyPath:ofObject:change:context
3、移除观察者:removeObserver:forKeyPath:context
注:移除观察者和注册观察者必须成对出现。
4、示例
示例 一
//被观察对象
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Person
@end
//观察者
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
// 注册 self 也就是 controller 为自己的观察者
[_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.name = @"哈哈";
}
// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@ - %@ - %@",keyPath,object,change);
}
// 移除观察者
- (void)dealloc{
[_person removeObserver:self forKeyPath:@"name"];
}
@end
示例 二
//手动触发
//1. 先在被观察者对象中关闭自动触发功能
// 对所有的都关闭
//+ (BOOL)automaticallyNotifiesObserversOfName{
// return NO;
//}
// 可以对某个指定key 关闭
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if([key isEqualToString:@"name"]){
NSLog(@"关闭了自动触发");
return NO;
}
return YES;
}
//2. 在赋值的地方
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// name 的值即将改变
[_person willChangeValueForKey:@"name"];
_person.name = @"哈哈";
// name的值改变完成
[_person didChangeValueForKey:@"name"];
}
示例三
//观察在 A 对象中的 B 对象的一个属性
//B 对象
@interface Dog : NSObject
@property(nonatomic,assign)int age;
@end
#import "Dog.h"
@implementation Dog
@end
//A 对象
@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end
@implementation Person
@end
//改变观察对象即可
[_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
示例四
//注册一个观察者,观察多个属性的改变:将多个属性聚合到一个容器中.即 +keyPathsForValuesAffectingValueForKey
+ (NSSet<NSString *> *)keyPathsForValuesAffectingName{
NSSet *keyPaths = [NSSet setWithArray:@[@"firstName",@"lastName"]];
return keyPaths;
}
注:属性 name 是由 firstName 和 lastName 两个属性组成.这样只需要注册 name 的观察者就可以了.
示例五
//可变数组
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
// sons是一个可变数组
_person.sons = [@[] mutableCopy];
// 观察 sons 的变化
[_person addObserver:self forKeyPath:@"sons" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// sons添加元素
[_person.sons addObject:@"lili"];
}
解析:
通过 [_person.sons addObject:@"lili"]; 这样的方法是无法触发 KVO 回调。
针对于可变数组的集合类型,需要通过 mutableArrayValueForKey 方法将元素添加到可变数组中,才能触发 KVO 回调。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[[_person mutableArrayValueForKey:@"sons"] addObject:@"lili"];
}
2 KVC
KVC:Key-Value Coding (即键值编码),可以通过一个 key 来访问某个属性。可以在运行时动态访问或修改对象的属性。
1、常见的 API
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
//特殊方法
// 默认返回YES,表示如果没有找到 Set 方法的话,会按照 _key,_iskey,key,iskey 的顺序搜索成员,设置成 NO 就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
// KVC 提供属性值正确性验证的 API,它可以用来检查 set 的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 这是集合操作的 API,里面还有一系列这样的 API,如果属性是一个 NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果 Key 不存在,且没有 KVC 无法搜索到任何和 Key 有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果你在 SetValue 方法时面给 Value 传 nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组 key,返回该组 key 对应的 Value,再转成字典返回,用于将 Model 转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
2、KVC 赋值原理
1、按顺序查找名为 set<Key>,_set<Key> 或者 setIs<Key> 的 setter 访问器顺序查找。如果找到就调用。只要实现任意一个,那么就会将调用这个方法,将属性的值设为传进来的值。
2、如果没有找到这些 setter 方法,KVC 机制会检查 + (BOOL)accessInstanceVariablesDirectly 方法有没有返回 YES(默认为YES)。如果重写了该方法让其返回 NO 的话,那么在这一步 KVC 会执行 setValue:forUndefinedKey:方法。
3、如果返回 YES,KVC 机制会优先搜索该类里面有没有名为 _<Key> 的成员变量。无论该变量是在类接口处定义还是在类实现处定义,也无论用了什么样的访问修饰符,只要存在以 _<Key> 命名的变量,KVC 都可以对该成员变量赋值。
4、KVC 机制会继续搜索 _is<Key>、<key> 和 is<key> 的成员变量,再给它们赋值。
5、如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的 setValue:forUndefinedKey:方法,默认是抛出异常。
KVC 设置值流程图
3、KVC 取值原理
1、首先按 get<Key>,<Key>,is<Key>,_ <Key> 的顺序方法查找 getter 方法,找到的话会直接调用。(如果是 BOOL 或者 Int 等值类型,会将其包装成一个 NSNumber 对象)
2、如果上面的 getter 没有找到,KVC 则会查找 countOf<Key>,objectIn<Key>AtIndex 或 <Key>AtIndexes 格式的方法。若其中的一个方法被找到,那么就会返回一个可以响应 NSArray 所有方法的代理集合(它是 NSKeyValueArray,是 NSArray 的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于 NSArray 的方法,就会以 countOf<Key>,objectIn<Key>AtIndex 或 At<Key>Indexes 这几个方法组合的形式调用。还有一个可选的 get<Key>:range: 方法。(故若想重新定义 KVC 的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合 KVC 的标准命名方法,包括方法签名)
3、如果上面的方法没有找到,那么会同时查找 countOf<Key>,enumeratorOf<Key>,memberOf<Key> 格式的方法。如果这三个方法都找到,那么就返回一个可以响应 NSSet 所的方法的代理集合,和上面一样,给这个代理集合发 NSSet 的消息,就会以 countOf<Key>,enumeratorOf<Key>,memberOf<Key> 组合的形式调用。
4、如果还没有找到,再检查类方法 + (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么会按_<Key>,_is<Key>,<Key>,is<Key> 的顺序搜索成员变量名(这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱)。如果重写了类方法 + (BOOL)accessInstanceVariablesDirectly 返回 NO 的话,那么会直接调用 valueForUndefinedKey: 。
5、若以上都没有找到的话,调用 valueForUndefinedKey: 。
KVC 取值流程图
4、keyPath 实现越级查找(如数组中的某个元素,对象的某个属性等)
[person setValue:@"" forKeyPath:@"location.country"];
5、KVC 相关异常处理
this class is not key value coding-complaint for the key XXX
解决方案:实现以下方法即可
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出现异常,该key不存在%@",key);
}
网友评论