ios kvo 要引入_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 *)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 *)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 *)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

@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

@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 给那个类添加方法

//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

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 *)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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值