KVO的那是些事儿

KVO的定义

   键值观察,对象采用的一种非正式协议,用于将其他对象的指定属性的更改通知给对象。可以观察到任何对象属性,包括简单属性,一对一关系和一对多关系。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。NSObject提供了NSKeyValueObserving协议的实现,该协议为所有对象提供了自动观察功能。开发者可以通过禁用自动观察者通知,并使用此协议中的方法实施手动通知来进一步优化通知。

常用方法和参数

注册观察

注册观察者
-addObserver:forKeyPath:options:context:

移除观察者。
-removeObserver:forKeyPath:

在给定上下文的情况下,移除观察者。
-removeObserver:forKeyPath:context:

变更通知

键值发生改变时,通知观察对象。
-observeValueForKeyPath:ofObject:change:context:

通知观察者变更

通知观察到的对象给定属性的值即将更改。
-willChangeValueForKey:

通知观察到的对象给定属性的值已更改。
-didChangeValueForKey:

通知观察对象对于指定的有序多对关系,将在给定的索引处执行指定的更改。
-willChange:valuesAtIndexes:forKey:

通知观察对象指定的多对多关系在索引上发生了指定的更改。
-didChange:valuesAtIndexes:forKey:

通知观察对象即将对指定的无序多对关系进行指定的更改。
-willChangeValueForKey:withSetMutation:usingObjects:

通知观察对象对指定的无序对多关系进行了指定的更改。
-didChangeValueForKey:withSetMutation:usingObjects:

观察定制

是否允许自动键值观察。
+automaticallyNotifiesObserversForKey:

示例:name属性不允许自动观察
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
   
   if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

// 手动观察name属性
- (void)setName:(NSString *)name {
    
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

为属性的值返回一组键路径,观察多个键值时使用,任一键值有更新,都会被通知到。
+keyPathsForValuesAffectingValueForKey:

示例:观察Person的dog属性中name、age的变化

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) Dog *dog;
@property (nonatomic, strong) NSMutableArray *arr;

@end

@implementation Person

- (instancetype)init {
    self = [super init];
    if (self) {
        _dog = [[Dog alloc] init];
        _arr = [NSMutableArray array];
    }
    return self;
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        
        // 此处一定要带下划线
        NSArray *arr = @[@"_dog.name", @"_dog.age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
    }
    return keyPaths;
}
@end

@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

#import "KVOViewController.h"
#import "Person.h"

@interface KVOViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation KVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    Person *person = [Person new];
    _person = person;
    
    // 观察person内dog属性的name和age变化
//    [_person addObserver:self forKeyPath:@"dog.name" options:(NSKeyValueObservingOptionNew) context:nil];
//    [_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
    [_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
   
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    _person.dog.name = name;
    _person.dog.age = i++;
}
@end

如果给定数组中指定的任何属性发生更改,则将观察对象配置为发布给定属性的更改通知。不推荐使用
+setKeys:triggerChangeNotificationsForDependentKey:

返回一个指针,该指针标识有关向观察对象注册的所有观察者的信息。
observationInfo

参数

可以观察到的变化类型。
NSKeyValueChange

NSKeyValueChangeSetting     // 观察set方法
NSKeyValueChangeInsertion   // 观察集合 insert 元素操作
NSKeyValueChangeRemoval     // 观察集合 remove 元素操作
NSKeyValueChangeReplacement // 观察集合 replace 元素操作

可以在变更字典中返回的值。
NSKeyValueObservingOptions

NSKeyValueObservingOptionOld      // 表示在change字典中包含了改变前的值。
NSKeyValueObservingOptionNew      // 表示在change字典中包含新的值。
NSKeyValueObservingOptionInitial  // 在注册观察者的方法return的时候就会发出一次通知。
NSKeyValueObservingOptionPrior    // 在值发生改变前、后各发出一次通知,每次change都会有两个通知。

可以显示在更改字典中的键。
NSKeyValueChangeKey

NSKeyValueChangeKindKey                 // 指明了变更的类型,值为“NSKeyValueChange”枚举中的某一个,类型为NSNumber
NSKeyValueChangeNewKey                  // 被监听属性改变后新值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)
NSKeyValueChangeOldKey                  // 被监听属性改变前旧值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)
NSKeyValueChangeIndexesKey              // 如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
NSKeyValueChangeNotificationIsPriorKey  // 如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.

可以对无序集合进行的突变类型。
NSKeyValueSetMutationKind

NSKeyValueUnionSetMutation      // 已将指定集中的观察者添加到观察对象。
NSKeyValueMinusSetMutation      // 正在从观察对象中除去指定集合中的观察者
NSKeyValueIntersectSetMutation  // 正在从观察对象中移除不在指定集合中的观察者。
NSKeyValueSetSetMutation        // 一组观察者正在替换观察对象中的现有对象。

KVO的内部实现

   当我们对Person对象的name属性进行观察的时候,实际上会在-addObserver:forKeyPath:options:context:内部动态创建一个Person的子类 NSKVONotifying_Person,动态修改观察对象的类型,观察子类,然后重写子类的set方法来实现。

image.png

image.png

可以发现,在addObserver:forKeyPath:options:context:执行前,person的isa指针指向Person,addObserver:forKeyPath:options:context执行后,person的isa指针指向了NSKVONotifying_Person,说明在addObserver:forKeyPath:options:context方法内部发生了指针混写。

自定义KVO的监听

新建一个NSObject的分类NSObject+JRKVO,定义方法:jr_addObserver: forKeyPath: options: context:,在方法实现中如下代码:

#import "NSObject+JRKVO.h"
#import 

static const char *JRKVO_observer = "JRKVO_observer";
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";

@implementation NSObject (JRKVO)

- (void)jr_addObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               options:(NSKeyValueObservingOptions)options
               context:(void *)context {
    
    // 创建、注册子类
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
    
    Class newClass = objc_getClass(newClassName.UTF8String);
    if (!newClass) {
        
        // 创建类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 注册类
        objc_registerClassPair(newClass);
    }
    
    // setter方法首字母大写
    NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
    NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
    SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
    
    // 动态修改监听的类型,监听子类
    object_setClass(self, newClass);
    
    // 子类添加setter方法
    Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
    const char *types = method_getTypeEncoding(getMethod);
    class_addMethod(newClass, setSEL, (IMP)setMethod, types);
    
    // 保存Observer
    objc_setAssociatedObject(self, JRKVO_observer, observer, OBJC_ASSOCIATION_ASSIGN);
    
    // 保存setter、getter方法
    objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

void setMethod(id self, SEL _cmd, id newValue) {
    
    // 获取setter、getter方法
    NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
    NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
    
    // 保存子类类型
    Class class = [self class];
    
    // isa指向原类
    object_setClass(self, class_getSuperclass(class));
    
    // 调用原类getter方法,获取旧值
    id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
    
    // 调用原类setter方法
    objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
    
    id observer = objc_getAssociatedObject(self, JRKVO_observer);
    
    NSMutableDictionary *change = [@{} mutableCopy];
    if (newValue) {
        change[NSKeyValueChangeNewKey] = newValue;
    }
    if (oldValue) {
        change[NSKeyValueChangeOldKey] = oldValue;
    }
    
    // 通知观察者
    objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getName, self, change, nil);
    
    // isa重新指向子类
    object_setClass(self, class);
}

@end

调用jr_addObserver: forKeyPath: options: context:方法,观察person的name属性

 [_person jr_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];

改变person的name属性的值

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
   static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    // _person.name = name;
    objc_msgSend(_person, @selector(setName:), name);
}
image.png

image.png

set方法已经执行,并且观察者回调方法中已经输出name新值、旧值。

自定义KVO的监听 带block回调

在上面创建的NSObject分类NSObject+JRKVO中,定义方法:jr_addObserverForKeyPath: options: callbak:,在方法实现中如下代码:

#import "NSObject+JRKVO.h"
#import 

static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
static const char *JRKVO_callback = "JRKVO_callback";

@implementation NSObject (JRKVO)

- (void)jr_addObserverForKeyPath:(NSString *_Nonnull)keyPath
                         options:(NSKeyValueObservingOptions)options
                         callbak:(JRKVOCallbackBlock _Nonnull )block {
    
    // 创建、注册子类
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
    
    Class newClass = objc_getClass(newClassName.UTF8String);
    if (!newClass) {
        
        // 创建类
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        // 注册类
        objc_registerClassPair(newClass);
    }
    
    // setter方法首字母大写
    NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
    NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
    SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
    
    // 动态修改监听的类型,监听子类
    object_setClass(self, newClass);
    
    // 子类添加setter方法
    Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
    const char *types = method_getTypeEncoding(getMethod);
    class_addMethod(newClass, setSEL, (IMP)setMethod, types);
    
    // 保存block
    objc_setAssociatedObject(self, JRKVO_callback, block, OBJC_ASSOCIATION_COPY);
    
    // 保存setter、getter方法
    objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

void setMethod(id self, SEL _cmd, id newValue) {

    // 获取setter、getter方法
    NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
    NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);

    // 保存子类类型
    Class class = [self class];

    // isa指向原类
    object_setClass(self, class_getSuperclass(class));

    // 调用原类getter方法,获取旧值
    id oldValue = objc_msgSend(self, NSSelectorFromString(getName));

    // 调用原类setter方法
    objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);

    NSMutableDictionary *change = [@{} mutableCopy];
    if (newValue) {
        change[NSKeyValueChangeNewKey] = newValue;
    }
    if (oldValue) {
        change[NSKeyValueChangeOldKey] = oldValue;
    }

    // 回调setter方法值的改变
    JRKVOCallbackBlock callback = objc_getAssociatedObject(self, JRKVO_callback);
    if (callback) {
        callback(change);
    }

    // isa重新指向子类
    object_setClass(self, class);
}
@end

调用jr_addObserverForKeyPath: options: callbak:方法,观察person的name属性变化。

image.png

观察多个键值的变化

keyPathsForValuesAffectingValueForKey:实现多个键值观察

示例:观察Person的dog属性中name、age的变化
Person 类中实现如下方法

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
   
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        
        // 此处一定要带下划线
        NSArray *arr = @[@"_dog.name", @"_dog.age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
    }
    return keyPaths;
}

VC中观察Person的dog属性

[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"dog 改变了%@", change);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
    _person.dog.name = name;
    _person.dog.age = i++;
}

点击屏幕,改变dog对象name、age的值,打印结果
image.png

通过keyPathsForValuesAffectingValueForKey:把多个keyPath放在一个集合set中,统一观察,dog的任何一个属性发生变化,都会通知到。比起单个keyPath观察,确实方便了很多。

数组的观察

通过KVC中的mutableArrayValueForKey: 实现数组的观察,数组中元素改变(增、删、改)都可以观察到。

示例代码:

#import "KVOViewController.h"
#import "Person.h"
#import "NSObject+JRKVO.h"
#import 

@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end

@implementation KVOViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    Person *person = [Person new];
    _person = person;
    
    // 观察 person的arr属性的变化, 数组元素的增删改
    [_person addObserver:self forKeyPath:@"arr" options:(NSKeyValueObservingOptionNew) context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {  
    NSLog(@"Person中的arr改变了%@", change);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    static NSInteger i = 0;
    NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
   
    // 通过KVC获取到数组,
    NSMutableArray *arr = [_person mutableArrayValueForKey:@"arr"];
    
    // add 对应 NSKeyValueChange 为 NSKeyValueChangeInsertion, 即:kind 为 2
    [arr addObject:name];
   
    // replace 对应 NSKeyValueChange 为 NSKeyValueChangeReplacement, 即:kind 为 4
    [arr replaceObjectAtIndex:0 withObject:@"1111"];
   
    // remove 对应 NSKeyValueChange 为 NSKeyValueChangeRemoval, 即:kind 为 3
    [arr removeObjectAtIndex:0];
}
@end

点击屏幕,操作数组元素的增、删、改,打印结果:

image.png

执行完mutableArrayValueForKey:后,观察arr的类型已经变成NSKeyValueMutableArray类型,其实mutableArrayValueForKey:内部动态创建了NSMutableArray的子类NSKeyValueMutableArray,然后指针混写指向子类,并返回子类,后续观察arr实际上是观察子类。

image.png

这里的kind = 2,kind = 3,kind = 4, 分别对应NSKeyValueChangeNSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。也就是说KVO不仅仅能观察对象的set方法,当对象为集合类型时,也能观察集合元素的增、删、改,只不过观察集合对象要通过KVC的mutableArrayValueForKey:方法来获取集合。类似通过person.arr的方式获取数组,操作数组元素的增、删、改是观察不到的。

你可能感兴趣的:(KVO的那是些事儿)