4.0 KVO - 476139183/Learning-iOS GitHub Wiki

KVO

KVO,全称为 Key-Value Observing,用于检测对象的某些属性的实时变化情况并作出响应。

常用的使用如下:

1. 添加观察者:

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(nullable void *)context;

参数主要为:

  • observer 观察者对象,值变化时通知的对象
  • keyPath 监听的属性,当属性为对象时,可以使用对象属性的属性(如:"hair.color")
  • options 监听变化类型的组合,可多选。属于 NSKeyValueObservingOptions 的枚举,具体看文末
  • context 通知的上下文,当同一个观察者对同一类的不同对象的同一属性或者对不同类的对象的相同属性进行监听时,可以使用该参数进行区分。可以是任何参数,会作为 -observeValueForKeyPath:ofObject:change:context: 方法的context参数发送给观察者。

2. 移除观察者:

  1. 被观察者会 优先移除 后面加入的 observer 和对应的 keyPath
- (void)removeObserver:(NSObject *)observer 
            forKeyPath:(NSString *)keyPath;

iOS 9.0 后,不移除也没事了,原因是 观察者持有 由 unsafe_unretain 变成 weak 了

  1. 和上面逻辑一样,不过多了一个 context 用以区分。
- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath
               context:(void *)context;

3. 观察者监听方法回调

当被观察对象的对应属性发送改变时,观察者对象可以通过以下方法来接收修改的信息:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;

其中 keyPathcontext 分别与 -addObserver:forKeyPath:options:context: 相互对应。object 为被观察者对象,change 为一个记录属性变化信息的字典,它的key 是 NSKeyValueChangeKey类型,可以使用key来访问对应信息,具体看文末

手动发送通知

我们只需要让被观察者主动调用了下面两个方法,就能触发KVO的回调:

[_person willChangeValueForKey:@"name"];

[_person didChangeValueForKey:@"name"];

监听集合属性

使用 mutableArrayValueForKey,将数组取出来,那么它就被被添加观察属性,它的isa 也是指向的 NSKeyValueNotifyingMutableArray

_person.colors = [[NSMutableArray alloc] init];
[_person addObserver:self forKeyPath:@"colors" options:NSKeyValueObservingOptionNew context:NULL];
//! 这段代码不会触发的kvo
[_person.colors addObject:@"1"];
///! 这段代码会触发
NSMutableArray *tempArray = [_person mutableArrayValueForKey:@"colors"];
[tempArray insertObject:@"2" atIndex:0];

属性依赖

当一个属性的变化依赖于其他属性变化时(例如一些计算属性),可以在被观察者类实现**+keyPathsForValuesAffectingValueForKey** 类方法将它们关联起来,代码如下:

@interface Person : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, readonly) NSString *fullName;
@end

@implementation Person
- (NSString *)fullName {
    return [self.firstName stringByAppendingString:[NSString stringWithFormat:@"%@",self.lastName]];
}

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    if ([key isEqualToString:@"fullName"]) {
        return [NSSet setWithObjects:@"firstName", @"lastName", nil];
    } else {
        return [super keyPathsForValuesAffectingValueForKey:key];
    }
}
@end

//! 观察代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        //添加监听
        [person addObserver:observer forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:NULL];
        
        person.firstName = @"史密斯";
        person.lastName = @"杰克逊";
    }
    return 0;
}

监听信息

如果我们想获取一个对象上有哪些观察者正在监听其属性,可以查看对象的observationInfo属性:

// 返回一个指针,包含了被观察对象添加的所有观察者对象、注册的options等信息。
// 默认的实现是从一个全局以被观察对象的地址作为键值的字典中获取observationInfo信息。
// 为了改善性能,可以重写observationInfo属性,将这些不透明的数据指针存储在一个实例变量里。重写这个属性不能发消息(send messages)给存储的数据。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

重复添加监听

给相同的被观察对象的相同属性重复添加相同观察者对象时,将会在属性发送改变时多次调用观察者对象的 -observeValueForKeyPath:ofObject:change:context: 方法,移除的时候 也要多次移除:

///! 重复监听两次
[_person addObserver:self forKeyPath:@"nane" options:NSKeyValueObservingOptionNew context:NULL];
[_person addObserver:self forKeyPath:@"nane" options:NSKeyValueObservingOptionNew context:NULL];

我们可以发现 _observances 是用数组实现的,所以不难理解为什么会触发多次了。

KVO的实现

KVO实现的本质是 利用 runtime 动态生成一个子类(也叫派生类),并让 instance 对象的 isa 指向这个全新的子类。 同时对应的set方法已经指向了 Foundation 的 _NSSetXXXValueAndNotify 的函数,而 _NSSetXXXValueAndNotify 的内部有如下实现:

  • 调用willChangeValueForKey:
  • 调用父类原来的set方法
  • 调用didChangeValueForKey:
    • 内部触发监听器(Oberser)的监听方法:
    observeValueForKeyPath: ofObject: change: context:
    

这里可以验证 子类的set方法已经被篡改了:

所以当修改 instance 对象的属性时候,新方法会进行处理,从而实现了通知观察者的作用。

而为了不影响用户的操作,KVO又通过重写 class 方法,让用户无感变化,可以正常当做 本类使用,而忽略内部实现

这正是 面向对象 封装 的特点

使用了KVO的对象关系图会变为下面这样:

内部重写了 setAge: class dealloc _isKVOA

KVC

全称 Key-Value-Coding, 使用它可以通过一个 key 来访问某一个属性,方法如下:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForKey:(NSString *)key;

设置成员变量,进行验证,可得到如下逻辑:

KVC 取值流程

如果设置了KVO,但是没有具体的成员接收,虽然会触发 KVC的流程,但是也会奔溃[<Person 0x600000687be0> valueForUndefinedKey:]

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x600000c75b20> valueForUndefinedKey:]: this class is not key value coding-compliant for the key height.'

KVC 的取值过程


补充

直接修改成员变量不会不触发KVO?
person->_age = 10

不会触发,因为直接修改成员变量,并未调用set方法

通过KVC会不会触发

[_person setValue:@"joker" forKey:@"name"];

会,经过测试发现KVC 并没有走set方法时,还是会触发,可以断定内部实现了 KVO 的代码。底层调用了 willChangeValueForKey , KVC的具体实现 和 didChangeValueForKey

通过调试可以发现,虽然 KVO 没有重写 - (void)setValue:(id)value forKey:(NSString *)key 方法,但是中间通过 函数 _NSSetValueAndNotifyForKeyInIvar 来处理 KVO

NSKeyValueObservingOptions:

//属性此次更改的新值。
NSKeyValueObservingOptionNew
//属性此次更改前的旧值。
NSKeyValueObservingOptionOld
//如果设置了这个值,将会立刻向观察者对象发送一次通知。通知的change中以新值的   方式发送被观察属性的当前值。
NSKeyValueObservingOptionInitial
//是否在属性改变前先通知一次观察者对象。如果没设置这个值,只会在属性发生改变后发送一次通知,设置了该值后会在属性发生改变前和改变后都通知一次。
NSKeyValueObservingOptionPrior

NSKeyValueChangeKey:

// 属性变化的类型,是一个NSNumber对象,包含NSKeyValueChange枚举相关的值
NSString *const NSKeyValueChangeKindKey;

// 属性的新值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionNew时,我们能获取到属性的新值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionNew时,则我们能获取到一个NSArray对象,包含被插入的对象或
// 用于替换其它对象的对象。
NSString *const NSKeyValueChangeNewKey;

// 属性的旧值。当NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加观察的方法设置了NSKeyValueObservingOptionOld时,我们能获取到属性的旧值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionOld时,则我们能获取到一个NSArray对象,包含被移除的对象或
// 被替换的对象。
NSString *const NSKeyValueChangeOldKey;

// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval
// 或者NSKeyValueChangeReplacement,则这个key对应的值是一个NSIndexSet对象,
// 包含了被插入、移除或替换的对象的索引
NSString *const NSKeyValueChangeIndexesKey;

// 当指定了NSKeyValueObservingOptionPrior选项时,在属性被修改的通知发送前,
// 会先发送一条通知给观察者。我们可以使用NSKeyValueChangeNotificationIsPriorKey
// 来获取到通知是否是预先发送的,如果是,获取到的值总是@(YES)
NSString *const NSKeyValueChangeNotificationIsPriorKey;

其中,NSKeyValueChangeKindKey的值取自于NSKeyValueChange,它的值是由以下枚举定义的:

enum {
    // 设置一个新值。被监听的属性可以是一个对象,也可以是一对一关系的属性或一对多关系的属性。
    NSKeyValueChangeSetting = 1,
    
    // 表示一个对象被插入到一对多关系的属性。
    NSKeyValueChangeInsertion = 2,
    
    // 表示一个对象被从一对多关系的属性中移除。
    NSKeyValueChangeRemoval = 3,
    
    // 表示一个对象在一对多的关系的属性中被替换
    NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;