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 的属性设置方式来变更属性值

 

posted @ 2017-11-16 09:41  Frank9098  阅读(141)  评论(0)    收藏  举报