016-KVO键值观察者模式
一、基本知识
1.概述
KVO(Key-value-observing)键值观察者模式。指的是Objective-C对观察设计模式的一种实现。KVO提供一种机制,指定一个被观察对象(例如:A类),当对象某个属性(例如:A中的字符串属性name)发生更改时,对象会获得通知,并作出相应处理
2.原理
1)底层实现原理--依赖于Runtime
①理论
KVO的监听,其实就是重写被监听者的被监听属性的set方法
KVO在注册监听的时候,其实就是在注册完监听的时候,底层会自动创建一个继承自被监听对象的子类,并在这个子类中去重写这个被监听属性的set方法,并且这个被监听对象的isa指针指向的是它的子类,后面通过属性方法调用改变该被监听对象的值的时候,就回去找该属性的set方法,但由于这个被监听对象的isa指针指向它的子类,所以调用的set方法自然也就是子类中重写的set方法
②原理图

③代码
// Dog.h文件 #import <Foundation/Foundation.h> @interface Dog : NSObject @property (nonatomic, assign) int age; // 年龄 @end // Dog.m文件 #import "Dog.h" @implementation Dog @end // Man.h文件 #import <Foundation/Foundation.h> @interface Man : NSObject @end // Man.m文件 #import "Man.h" @implementation Man #pragma mark - KVO监听事件 // 在RunTimeRespondController中注册了让Man对象监听Dog对象的age属性的变化 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"监听到%@的%@属性变化为:%@", object, keyPath, change); } @end // RunTimeRespondControlle.m文件 #import "RunTimeRespondController.h" #import "Man.h" #import "Dog.h" @interface RunTimeRespondController () @property (nonatomic, strong) Man *m; @property (nonatomic, strong) Dog *d; @end @implementation RunTimeRespondController - (void)viewDidLoad { [super viewDidLoad]; [self initUI]; // 界面 } #pragma mark - 界面 - (void)initUI { self.view.backgroundColor = [UIColor whiteColor]; self.title = @"KVO响应式编程"; [self test]; } #pragma mark - 注册KVO监听者 - (void)test { // KVO底层实现原理 // KVO的监听,其实就是重写被监听者的被监听属性的set方法 // 底层找方法其实也就是通过isa指针 // KVO在注册监听的时候,其实就是在注册完监听的时候,底层会自动创建一个继承自被监听对象的子类,并在这个子类中去重写这个被监听属性的set方法,并且这个被监听对象的isa指针指向的是它的子类,后面通过属性方法调用改变该被监听对象的值的时候,就回去找该属性的set方法,但由于这个被监听对象的isa指针指向它的子类,所以调用的set方法自然也就是子类中重写的set方法 _m = [[Man alloc] init]; _d = [[Dog alloc] init]; // 注册监听 // 让m对象监听d对象的age属性的变化 [self.d addObserver:self.m forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; } #pragma mark - 点击屏幕 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { _d.age ++; } #pragma mark - 销毁KVO观察者 - (void)dealloc { [self.d removeObserver:self.m forKeyPath:@"age"]; } @end
2)自定义KVO
①理论
关于KVO的面试题
苹果为什么用子类去监听set方法,而不用分类去监听set方法
因为如果本身的类中有重写set方法需要处理一些事情,而分类中又去重写set方法,那么分类中也无法调用本身类的set方法,这样就造成本身类中的set处理事情来不了;而用子类去监听set方法,子类中可以通过super调用父类的set方法,这样本身父类中的set处理事情也就可以来到啦
②代码
// Girl.h文件 #import <Foundation/Foundation.h> @interface Girl : NSObject @property (nonatomic, assign) int age; @end // Girl.m文件 #import "Girl.h" @implementation Girl @end // NSObject+LDKVO.h文件 // 给所有的NSObject对象添加一个方法 #import <Foundation/Foundation.h> @interface NSObject (LDKVO) #pragma mark - 扩展一个方法 // 仿照KVO的注册监听方法 - (void)LD_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; @end // NSObject+LDKVO.m文件 #import "NSObject+LDKVO.h" #import <objc/message.h> @implementation NSObject (LDKVO) #pragma mark - 扩展一个方法 // 仿照KVO的注册监听方法 - (void)LD_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context { // 方法 // objc_allocateClassPair // 动态添加一个类 // objc_registerClassPair // 动态注册一个类 // class_addMethod // 动态地给某一个类添加方法 // class_addIvar // 动态地给某一个类添加属性 // object_setClass // 修改某一个类的isa指针 // objc_setAssociatedObject // 关联属性 // objc_getAssociatedObject // 取得关联属性 // objc_registerClassPair // 注册类 // 1.自定义一个类,继承[self class] // self:谁调用这个方法,谁就是self // 2.重写被监听属性keyPath的set方法 // 3.通知观察者observer // 其实就是调用observer的observeValueForKeyPath方法 // 动态添加一个继承[self class]的类 NSString *oldClassName = NSStringFromClass([self class]); // 拿到父类名称字符串 NSString *newClassName = [NSString stringWithFormat:@"LDKVO_%@", oldClassName]; // 拼接继承父类的子类名称字符串 const char * newName = [newClassName UTF8String]; // OC字符串转化为C的字符串 Class myClass = objc_allocateClassPair([self class], newName, 0); // 动态地给子类添加被监听属性的set方法 -- 相当于给子类重写被监听属性的set方法 class_addMethod(myClass, @selector(setAge:), (IMP)setAge, "v@:i"); // 注册这个子类 objc_registerClassPair(myClass); // 修改被观察对象的isa指针 // 其实就是self的isa指针 object_setClass(self, myClass); // 将观察者属性observer保存到当前类myClass里面去 // 这里的self指的是myClass,因为上一步已经改变了self的isa指针 objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - 相当于重写被监听属性的set方法 void setAge(id self, SEL _cmd, int age) { // 保存当前类 Class myClass = [self class]; // 调用父类的监听属性的set方法,去设置新值 // 这里将self的isa指针改成指向父类的 object_setClass(self, class_getSuperclass([self class])); // 调用父类 ((void(*)(id,SEL,int))objc_msgSend)((id)self, @selector(setAge:), age); // 拿出观察者属性 id objc = objc_getAssociatedObject(self, (__bridge const void *)@"objc"); // 通知观察者 ((void(*)(id,SEL,id,NSString *,id,id))objc_msgSend)(objc, @selector(observeValueForKeyPath:ofObject:change:context:), self, @"age", nil, nil); // 改回子类 object_setClass(self, myClass); } @end // CustomKVOController.m文件 #import "CustomKVOController.h" #import "Girl.h" #import "NSObject+LDKVO.h" @interface CustomKVOController () @property (nonatomic, strong) Girl *g; @end @implementation CustomKVOController - (void)viewDidLoad { [super viewDidLoad]; [self initUI]; // 界面 } #pragma mark - 界面 - (void)initUI { self.view.backgroundColor = [UIColor whiteColor]; self.title = @"KVO底层-自定义KVO"; [self test]; // 关于KVO的面试题 // 苹果为什么用子类去监听set方法,而不用分类去监听set方法 // 因为如果本身的类中有重写set方法需要处理一些事情,而分类中又去重写set方法,那么分类中也无法调用本身类的set方法,这样就造成本身类中的set处理事情来不了;而用子类去监听set方法,子类中可以通过super调用父类的set方法,这样本身父类中的set处理事情也就可以来到啦 } #pragma mark - 自定义KVO - (void)test { _g = [[Girl alloc] init]; _g.age = 18; // 注册监听 // 用我们分类中的扩展方法 [self.g LD_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; } #pragma mark - KVO监听事件 // 注册了让self监听Girl对象的age属性的变化 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"监听到%@的%@属性变化为:%@", object, keyPath, change); } #pragma mark - 点击屏幕 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { _g.age ++; } @end
二、使用步骤&注意事项
1.使用步骤
1)方法
// 注册监听 // 让m对象监听d对象的age属性的变化 // 参数1:observer 观察者(这里观察self.d对象的属性变化) // 参数2:keyPath 被观察的属性名称(这里观察self.d对象的age属性值的改变) // 参数3:options 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置) // 参数4:context 上下文,可以为KVO的回调方法传值(例如:设定为一个放置数据的字典) [self.d addObserver:self.m forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; #pragma mark - KVO监听事件 // 在RunTimeRespondController中注册了让Man对象监听Dog对象的age属性的变化 // 参数1:keyPath 属性名称 // 参数2:object 被观察的对象 // 参数3:change 变化前后的值都存储在change字典中 // 参数4:context 注册观察者时,context传过来的值 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"监听到%@的%@属性变化为:%@", object, keyPath, change); }
2)步骤
①注册观察者,实施监听
②在回调方法中处理属性发生的变化
③移除观察者
2.注意事项
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量(例如:仅调用_name = @"newName"),这时是不会触发KVO机制,更加不会调用回调方法的。所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值

浙公网安备 33010602011771号