iOS-OC底层18:KV0原理
1.概念
KVO(Key-Value-Observer)也就是观察者模式(键值观察),是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件,一般继承自NSObject的对象都默认支持KVO
2.基本使用
2.1.实现基本的监听
//定义类
@interface PWPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
self.person = [[PWPerson alloc] init];
self.person.name = @"pw";
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = [NSString stringWithFormat:@"%@++",self.person.name];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
打印日志
{
kind = 1;
new = "pw++";
}
{
kind = 1;
new = "pw++++";
}
需要注意的两点
1.addObserver:forKeyPath:options:context: 中context存在的意义
如果在一个UIViewController中,同一个类的两个对象监听同一个forKeyPath,我们在observeValueForKeyPath:ofObject:change:context的判断就会比较多,keyPath和object两者都要判断,如果我们在添加观察者时context值不为空,我们在这里就可以直接判断context了
2.removeObserver:forKeyPath:是必须的吗?
一般情况下不会有问题,如果观察的对象是一个单利,当被观察的KeyPath改变时可能会向一个野指针发送消息。
2.2.KVO自动触发改成手动触发
重写automaticallyNotifiesObserversForKey方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqual:@"name"]) {
return NO;
}
return YES;
}
当我们点击屏幕修改name属性时,没有收到信息,我们怎么改动才会收到信息呢
重写set方法
-(void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = [name copy];
[self didChangeValueForKey:@"name"];
}
我们收到信息
{
kind = 1;
new = "pw++++++++";
}
2.3 嵌套层次
@interface PWPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *secondName;
@property (nonatomic, copy) NSString *allName;
@end
@implementation PWPerson
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"allName"]) {
NSArray *affectingkeys = @[@"firstName",@"secondName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingkeys];
}
return keyPaths;
}
-(NSString *)allName {
return [NSString stringWithFormat:@"%@-----%@",self.firstName,self.secondName];
}
@end
self.person.firstName = @"first";
self.person.secondName = @"second";
[self.person addObserver:self forKeyPath:@"allName" options:(NSKeyValueObservingOptionNew) context:NULL];
//触发更改
self.person.firstName = [NSString stringWithFormat:@"%@++",self.person.firstName];
self.person.secondName = [NSString stringWithFormat:@"%@++",self.person.secondName];
打印日志
{
kind = 1;
new = "first++++++++-----second++++++++";
}
allName是有firstName和secondName合成而来的,firstName和secondName的更改决定了allName,所以我们增加了嵌套,firstName和secondName决定了allName的值
2.3 观察数组
我们在被观察类里添加可变数组
@property (nonatomic, strong) NSMutableArray *hobbyArray;
//观察对象的可变数组
[self.person addObserver:self forKeyPath:@"hobbyArray" options:(NSKeyValueObservingOptionNew) context:NULL];
//向数组中添加数据
[self.person.hobbyArray addObject:@"footBall"];
但是我们没有收到信息,这是为什么呢?
NSObject
provides a basic implementation of automatic key-value change notification. Automatic key-value change notification informs observers of changes made using key-value compliant accessors, as well as the key-value coding methods. Automatic notification is also supported by the collection proxy objects returned by, for example,mutableArrayValueForKey:
.
我们需要用mutableArrayValueForKey来修改集合的数据
[[self.person mutableArrayValueForKey:@"hobbyArray"] addObject:@"footBall"];
//打印日志
{
indexes = "<_NSCachedIndexSet: 0x600000c02920>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
kind = 2;
new = (
footBall
);
}
我们看到 kind = 2,在前面我们看到kind = 1,kind到底是何方神圣呢?
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
我们触发让kind等于3或者等于4呢?NSKeyValueChangeRemoval我们看到removal应该是移除,NSKeyValueChangeReplacement应该是更换
2.3.1从数组中移除元素
移除元素之前,我们先确定数组中有元素所以代码如下
self.person.hobbyArray = [@[@"footBall",@"basketBall",@"pingpngBall"] mutableCopy];
//触发观察
[[self.person mutableArrayValueForKey:@"hobbyArray"] removeLastObject];
打印日志
{
indexes = "<_NSCachedIndexSet: 0x600000c30ba0>[number of indexes: 1 (in 1 ranges), indexes: (2)]";
kind = 3;
}
我们心心念念的kind = 3出现了。kind = 4呢?
[[self.person mutableArrayValueForKey:@"hobbyArray"] replaceObjectAtIndex:0 withObject:@"billiards"];
//打印日志
{
indexes = "<_NSCachedIndexSet: 0x6000032d8e40>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
billiards
);
}
3.KVO原理
3.1.KVO观察的是属性还是成员变量
我们在PWPerson中添加一个公开的成员变量
@interface PWPerson : NSObject
{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *name;
@end
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person.name = @"pw";
self.person->name = @"name";
打印日志
{
kind = 1;
new = pw;
}
KVO观察的是属性
3.1.谣传KVO会生成中间类
NSLog(@"----%@",NSStringFromClass(object_getClass(self.person)));
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"----%@",NSStringFromClass(object_getClass(self.person)));
打印日志
----PWPerson
----NSKVONotifying_PWPerson
3.1.1查看NSKVONotifying_PWPerson的父类
NSString *className = @"NSKVONotifying_PWPerson";
Class newClass = NSClassFromString(className);
if (newClass) {
while (newClass) {
NSLog(@"%@",NSStringFromClass(newClass));
newClass = class_getSuperclass(newClass);
}
}
打印日志
NSKVONotifying_PWPerson
PWPerson
NSObject
3.1.1查看NSKVONotifying_PWPerson的方法列表
NSString *className = @"NSKVONotifying_PWPerson";
Class newClass = NSClassFromString(className);
unsigned int count;
Method *methods = class_copyMethodList(newClass, &count);
for (int i = 0; i< count; i++) {
Method method = methods[i];
SEL sel= method_getName(method);
NSLog(@"%@",NSStringFromSelector(sel));
}
打印日志
setName:
class
dealloc
_isKVOA
我们对PWPerson对象进行[self.person class],结果是PWPerson,重写了class方法
setName方法的目的可能就是向观察者发送消息,当值发生变化是发送消息
_isKVOA可能标记这个类是kvo生成的中间类。
dealloc:对观察者释放。
4.KVO简单的自定义
1:动态生成子类:NSKVONotifiy_A
1:防止实例变量的影响,我添加断言异常排除
2:动态子类的过程: A:生成类 B:添加Class方法 C:注册
3.同时也判断类的存在性做了处理
2.动态给子类添加Setter方法
3.消息转发给父类(runtime消息转发)
具体实现可以参看KVOController