在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];