阅读 223

iOS: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指针指向这个子类。

在添加观察者的地方打个断点来看一下:

isa指向.png
。此时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的类型:

tempArray.png

最后补充几个注意:

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)

关注下面的标签,发现更多相似文章
评论