我用#CSDN#《iOS KVO机制的基本使用及实现原理》 https://blog.csdn.net/u011212411/article/details/86562942
在Cocoa Touch框架中,观察者模式的具体应用有两个:通知(Notification)机制和KVO(Key-Value-Observing)机制。
KVO不同于通知机制那样通过一个NSNotificationCenter通知所有观察者对象,是在对象属性发生变化时通知会被直接发送给观察者对象,也可以手动模式,没有改变仍可调用
一、KVO基本使用
使用KVO分三个步骤:
1、通过addObserver方法注册观察者,观察者可以接受keyPath属性的变化事件。
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2、在观察者中实现observeValueForKeyPath方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
3、当观察者不需要监听时,可以调用removeObserver方法将KVO移除。需要注意调用removeObserver需要在观察者消失之前,否则会导致Crash。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
addObserver方法
observer参数:要被观察的对象
keyPath参数:需要观察的属性。由于是字符串形式,传错容易导致Crash。一版利用系统的反射机制NSStringFromSelector(@selector(keyPath))
options参数:为属性变化设置的选项,NSKeyValueObservingOptions是一个枚举类型
NSKeyValueObservingOptionNew 接受新值,默认
NSKeyValueObservingOptionOld 接受旧值
NSKeyValueObservingOptionInitial 在注册时立即接收一次回调,在改变时也会发送通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次
content参数:上下文内容,它的类型是C语言形式的任何指针类型
*注意:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。
监听回调
观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,如果没有实现会导致Crash。
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<ViewController: 0x7fab9ad0f150>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
keyPath参数:监听属性名称
object参数:被观察对象
change参数:字典类型,包含了属性变化的内容,根据options时传入的枚举来返回
context参数:注册时传递的上下文内容
实例代码
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@property (nonatomic, strong) NSString *sex;
@property (nonatomic, strong) NSMutableArray *arr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"name";
self.age = @"age";
self.sex = @"sex";
self.arr = [[NSMutableArray alloc] init];
//添加观察者
//监听一个属性
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(sex)) options:NSKeyValueObservingOptionNew context:nil];
//监听多个属性
[self addObserver:self forKeyPath:@"self" options:NSKeyValueObservingOptionNew context:nil];
//监听一个容器
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(arr)) options:NSKeyValueObservingOptionNew context:nil];
}
//监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"\nkeyPath:%@\nobject:%@\nchange:%@\ncontext:%@",keyPath,object,change,context);
NSLog(@"属性:%@ %@ %@",self.name,self.age,self.sex);
}
/**
触发模式 自动还是手动
手动模式
-willChangeValueForKey
-didChangeValueForKey
@param key 关注的属性
@return 是否自动d调用
*/
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"sex"]) {
return NO;
}
return YES;
}
/**
KVO本身会自动观察同一个实例的所有密钥路径,并在任何密钥路径的值发生变化时向观察者发送密钥的更改通知。
@param key 建路径
@return 返回一组键值 属性的键路径
*/
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"self"]) {
keyPaths = [[NSSet alloc] initWithObjects:@"name",@"age", nil];
}
return keyPaths;
}
//点击
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int num;
//自动触发
self.name = [NSString stringWithFormat:@"name:%d",num++];
self.age = [NSString stringWithFormat:@"age:%d",num];
//KVO方法 mutableArrayValueForKey
NSMutableArray *temparr = [self mutableArrayValueForKey:@"arr"];
[temparr addObject:[NSString stringWithFormat:@"arr:%d",num]];
//手动触发
[self willChangeValueForKey:@"sex"];
self.sex = [NSString stringWithFormat:@"sex:%d",num%2];
[self didChangeValueForKey:@"sex"];
}
//移除KVO
- (void)dealloc
{
[self removeObserver:self forKeyPath:@"self"];
[self removeObserver:self forKeyPath:@"sex"];
[self removeObserver:self forKeyPath:@"arr"];
}
@end
输出
2019-01-20 18:00:03.937949+0800 KVO使用[10696:320443]
keyPath:self
object:<ViewController: 0x7ffa5ac1bb90>
change:{
kind = 1;
new = "<ViewController: 0x7ffa5ac1bb90>";
}
context:(null)
2019-01-20 18:00:03.938252+0800 KVO使用[10696:320443] 属性:name:0 (null) (null)
2019-01-20 18:00:03.938467+0800 KVO使用[10696:320443]
keyPath:self
object:<ViewController: 0x7ffa5ac1bb90>
change:{
kind = 1;
new = "<ViewController: 0x7ffa5ac1bb90>";
}
context:(null)
2019-01-20 18:00:03.938586+0800 KVO使用[10696:320443] 属性:name:0 age:1 (null)
2019-01-20 18:00:03.938868+0800 KVO使用[10696:320443]
keyPath:sex
object:<ViewController: 0x7ffa5ac1bb90>
change:{
kind = 1;
new = "sex:0";
}
context:(null)
2019-01-20 18:00:03.939170+0800 KVO使用[10696:320443] 属性:name:0 age:1 sex:0
二、KVO底层实现
KVO的实现依赖于Runtime,当监听某个对象的某个属性时,有几点步骤
1、创建一个子类 NSKVONotyfing_对象名,该类继承自目标对象的本类
2、重写setName方法
3、外界改变isa指针
#import "NSObject+FLYKVO.h"
#import <objc/message.h>
@implementation NSObject (FLYKVO)
- (void)FLY_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
//1、创建一个类
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [@"FLYKVO_" stringByAppendingString:oldClassName];
Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
//注册类
objc_registerClassPair(MyClass);
/**
2、重写setName方法
@param MyClass 给类添加方法
@param setName: 方法编码
@param IMP 方法实现 函数指针
@param type 函数返回类型 v=void @=object :=selector @=object
*/
class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
//3、修改isa指针 指向子类
object_setClass(self, MyClass);
//4、将观察者保存到当前对象 不要用strong 要用weak 否则循环引a用
objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
}
void setName(id self, SEL _cmd, NSString *newName) {
NSLog(@"来了老弟!!!");
Class class = [self class];
//改成父类
object_setClass(self, class_getSuperclass(class));
//调用父类的setName方法
objc_msgSend(self, @selector(setName:),newName);
//观察者
id observer = objc_getAssociatedObject(self, @"observer");
if (observer) {
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new:":newName,@"kind":@1},nil);
}
//改回子类
object_setClass(self, class);
}
@end
调用自定义的监听方法
[self FLY_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];