内容简介:本文仅是记录自己在学习的过程中的理解:如有错误,还望各位大佬指正,THX.KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即
本文仅是记录自己在学习的过程中的理解:如有错误,还望各位大佬指正,THX.
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
1. KVO 的基本使用
相信大家在平时的开发中都使用过KVO,使用KVO分为3个步骤:
1.通过- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注册观察者,观察者可以接收keyPath属性的变化事件。
参数部分: --Observer参数:观察者对象 --keyPath参数:需要观察的属性,由于是字符串的形式,写错的话很容易导致崩溃,一般利用系统的反射机制NSStringFromSelector(@selector(keyPath)); --options参数:枚举类型 NSKeyValueObservingOptionNew 接收新值,默认为只接收新值 NSKeyValueObservingOptionOld 接收旧值 NSKeyValueObservingOptionInitial 在注册的时候立即接收一次回调,在改变是也会发生通知 NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次 --context参数:传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式 **注意:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致由于观察者的释放而带来的崩溃。
2.在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,当keyPath属性发生改变之后,KVO会回调这个方法来通知观察者属性的改变。
3.当观察者不需要监听的时候,可以调用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法将KVO移除,需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致崩溃。一般在dealloc中调用。
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
2. KVO的触发模式
KVO在属性发生改变的时候默认是自动调用的,如果需要手动的控制这个调用时机,或者自己来实现KVO属性的调用,可以通过KVO提供的方法来调用。 在所要观察的对象.m文件中加入:
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { return YES;//默认,自动模式 return NO;//手动模式 } 复制代码
同时在属性变化之前,调用:
- (void)willChangeValueForKey:(NSString *)key; 复制代码
在属性变化之后,调用:
- (void)didChangeValueForKey:(NSString *)key; 复制代码
其实无论属性的值是否发生改变,是否调用Setter方法,只要调用了willChangeValueForKey:和didChangeValueForKey:就会触发回调。
一般我们在开发的时候,需要用到KVO监听属性值得变化,一般不会将所有的值得监听都是手动的触发,同时我们也看到automaticallyNotifiesObserversForKey:传入了一个参数key, 就是为了让我们根据key来决定是否手动开启KVO.
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{ if ([key isEqualToString:@"name"]) { return NO;//手动模式 } return YES;//默认,自动模式 } 复制代码
3. KVO属性依赖
如果在当前Person类中引入另外一个Dog类:
// Dog.h @interface Dog : NSObject @property (nonatomic,assign) NSInteger age; @property (nonatomic,assign) NSInteger level; @end // Person.h @interface Person : NSObject @property (nonatomic,copy) NSString *name; @property (nonatomic,strong) Dog *dog; @end //Person.m @implementation Person -(instancetype)init { if (self = [super init]) { _dog = [[Dog alloc] init]; } return self; } +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{ if ([key isEqualToString:@"name"]) { return NO;//手动模式 } return YES;//默认,自动模式 } 复制代码
那么此时我们怎么通过Person来观察Dog类的age属性呢?
[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil]; 复制代码
如果Dog类有多个属性;那么我们现在希望,只要Dog类中有属性的变化,就会通知到Person类,如果我们每一个属性都添加一遍观察者,是不是很麻烦,那么这里就需要用到 属性依赖 :
我们在Person类的.m中添加一个方法:
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPath = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqual:@"dog"]) { keyPath = [[NSSet alloc] initWithObjects:@"_dog.age",@"_dog.level", nil]; } return keyPath; } 同时在添加观察者时,不用对dog具体的属性添加: [_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil]; 复制代码
4. KVO 的原理
KVO的其实就是观察属性的变化,也就是setter方法的变化,但是上面我们也提到过就是不需要调用setter方法同样可以触发KVO,那么KVO到底是不是观察setter方法呢?现在我们把代码恢复到最初的时候,此时只观察Person类的name属性,如果此时把name改成成员变量:
// Person.h @interface Person : NSObject { @public NSString *name; } //@property (nonatomic,copy) NSString *name; @end //调用改变name -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { static int a; _p.name = [NSString stringWithFormat:@"%d",a++]; } 复制代码
当改变name的值得时候,可以发现此时并不会有回调。 那么可以知道,其实KVO观察的还是属性的setter方法。 那么如何实现当调用Person类对象的setter方法的时候能够观察到改变呢?一般有两种方式实现: 分类和子类继承 。 那么我们可以试一下分类,创建一个Person的分类,并在分类里重写setName:方法,发现是可行的。但此时有一个隐患存在,因为此时我们已经在分类中实现了setName:方法,等于就是替换掉了Person类的setName:方法,此时Person类的setName:方法就不会被调用,而此时如果又需要重写Person类的setName:方法,那么就会出现影响。
KVO 底层实现:首先KVO需要创建一个子类(NSKVONotyfing_Person),这个子类是继承于被观察对象的,这个子类需要重写属性的setter方法,这个时候,外界在调用setter方法的时候,调用的是子类重写的setter方法。就是让外界的person对象的isa指针指向这个子类。
在添加观察者的地方打个断点来看一下:
。此时Person类对象的isa指针指向的就是子类对象。
5. 自定义KVO
首先创建一个NSObject的分类:
// NSObject+KVO.h #import <Foundation/Foundation.h> @interface NSObject (KVO) -(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; @end 复制代码
// NSObject+KVO.m #import "NSObject+KVO.h" #import <objc/message.h> @implementation NSObject (KVO) -(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { //创建一个类 NSString *oldClassName = NSStringFromClass(self.class); NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName]; Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0); //注册类 objc_registerClassPair(MyClass); //MyClass继承于self.class 根据案例来看,此时MyClass继承于Person,那么此时MyClass这个子类是否具有父类Person的setName:方法呢? 没有,只不过我们在调用方法的时候,子类继承于父类,如果子类没有实现方法,回去父类中调用该方法,所以在潜意识上,我们人为子类具有父类的方法,所谓的重写子类的方法,其实就是给这个子类添加一个方法。 //重写setName方法 class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@"); //class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>) //参数名称 参数 //Class 给那个类添加方法 //SEL 方法编号 //IMP 方法实现(指针) //types 返回值类型 //修改isa指针 object_setClass(self, MyClass); //将观察者保存到当前对象 objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN); } void setName(id self,SEL _cmd,NSString * newName){ NSLog(@"%@",newName); //调用父类的setName:方法 Class class = [self class];//拿到当前类型 object_setClass(self, class_getSuperclass(class));//修改当前类型,变成父类 objc_msgSend(self, @selector(setName:),newName); //拿到Observer, id observer = objc_getAssociatedObject(self, @"observer"); if (observer) { objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",@{@"name":newName,@"kind":@1},nil); } //改回子类 object_setClass(self, class); } @end 复制代码
这么写的KVO不会覆盖父类的set方法,也不会因为没有在dealloc中remove掉observer而崩溃掉。
6. 容器类的KVO
// Person.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface Person : NSObject @property (nonatomic,copy) NSString *name; @property (nonatomic,strong)NSMutableArray * array; @end // Person.m -(NSMutableArray *)array { if (!_array) { _array = [NSMutableArray array]; } return _array; } // ViewController.m [_p addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil]; 复制代码
其实注册观察者的步骤与属性时一样的,只不过在修改array的时候有些变化,因为KVO监听的是set方法,而对array进行操作却不是set方法,这时候其实KVO提供了一个方法:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSMutableArray *tempArray = [_p mutableArrayValueForKey:@"array"]; [tempArray addObject:@"xxxx"]; //利用tempArray 去进行操作 } 复制代码
通过断点来看一下tempArray的类型:
最后补充几个注意:
1.kvo的本质是什么?
利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的子类,当修改instance对象的属性时,会调用willChangeValueForKey和didChangeValueForKey( 在父类原来的setter方法)并调用内部会触发监听器的监听方法(observerValueForKeyPath:)。
2.直接修改成员变量会触发KVO么?
不会触发KVO,添加KVO的Person实例,其实是NSKVONotyfing_Person类,再调用setter方法,不是调用Person的setter方法,而是NSKVONotyfing_Person的setter方法,因为修改成员变量不是setter方法赋值。
3.如果在项目中对Person类进行了监听,也创建了一个NSKVONotifying_Person类,那么会编译通过么?
编译通过,因为KVO是运行时刻创建的,并不在编译时刻,在编译时刻只有一个NSKVONotifying_Person,所以不报错,可以通过,但是此时KVO起不了作用。(KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。