探讨KVO(OC)底层实现原理(二)

1,259 阅读18分钟

参考官方文档(developer.apple.com/library/arc…

KVO中API参数详解

先来看方法:- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

  • observer表示观察者(指定谁来监听属性改变,当然也可以指定自己来监听),NSObject类型,表示只要继承了NSObject类型的对象(包括NSObject对象)都可以成为观察者
  • keyPath 表示要监听的属性
  • options 表示以何种配置来观察属性,既会影响通知中提供的更改字典的内容,又会影响生成通知的方式
  • context ,其类型为void*,上下文对象,主要用于区分通知,提高安全

其中observer和keyPath很容易理解,下面来详细讲解options和context

options

options 有如下四中配置

  • 1.NSKeyValueObservingOptionNew 观察者回调监听中change字典中包含改变后的值

  • 2.NSKeyValueObservingOptionOld 观察者回调监听中change字典中包含改变前的值

  • 3.NSKeyValueObservingOptionInitial 注册后立刻触发KVO通知

但是需要注意的是 NSKeyValueObservingOptions参数同时指定了NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial,首次触发KVO change字典中并不包含old值

  • 4.NSKeyValueObservingOptionPrior 值改变前是否通知(改变前通知一次,改变后再通知一次)

例子1:

#import "ViewController.h"

@interface Person : NSObject

@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

@end

@interface ViewController ()

@property (nonatomic, strong) Person * person;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [[Person alloc] init];
    //添加属性观察
    [_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld  context:nil];
    //触发KVO
    [_person setValue:@300 forKey:@"age"];
    
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   
    NSLog(@"%s",__func__);
    NSLog(@"keyPath = %@",keyPath);
    NSLog(@"change  = %@",change);
    
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
    //移除
    [_person removeObserver:self forKeyPath:@"age"];
}

@end

打印结果如下:

2019-12-26 15:26:11.683096+0800 KVC&KVO[3325:172468] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:26:11.683215+0800 KVC&KVO[3325:172468] keyPath = age
2019-12-26 15:26:11.683373+0800 KVC&KVO[3325:172468] change  = {
    kind = 1;
    new = 300;
    old = 0;
}

结果分析: 因为options同时指定了NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld,因此KVO回调接口字典change中同时包含new 和 old,如果只指定了其中一个,那么回调字典中就只有对应的一个

接着我们将options参数改为: NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial

2019-12-26 15:32:44.449559+0800 KVC&KVO[3351:175049] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:32:44.449682+0800 KVC&KVO[3351:175049] keyPath = age
2019-12-26 15:32:44.449822+0800 KVC&KVO[3351:175049] change  = {
    kind = 1;
    new = 0;
}
2019-12-26 15:32:44.450094+0800 KVC&KVO[3351:175049] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:32:44.450185+0800 KVC&KVO[3351:175049] keyPath = age
2019-12-26 15:32:44.450317+0800 KVC&KVO[3351:175049] change  = {
    kind = 1;
    new = 300;
    old = 0;
}

结果分析:

1.由于指定了NSKeyValueObservingOptionInitial,所以一旦添加观察,就立刻触发KVO(你可以将 [_person setValue:@300 forKey:@"age"]注释掉,它一样会触发KVO,也就是change字典中new =0的那一次).

2.options指定了 NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial三个,但是首次立刻触发的KVO回调字典并不包含old值

接着在Person类中添加:

//该方法用于修改是否允许自动KVO通知。默认允许返回YES,这里我们修改为NO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    NSLog(@"%s",__func__);
    return NO;
}

再次运行:

2019-12-26 15:39:48.684258+0800 KVC&KVO[3389:178028] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:39:48.684373+0800 KVC&KVO[3389:178028] keyPath = age
2019-12-26 15:39:48.684523+0800 KVC&KVO[3389:178028] change  = {
    kind = 1;
    new = 0;
}
2019-12-26 15:39:48.684614+0800 KVC&KVO[3389:178028] +[Person automaticallyNotifiesObserversForKey:]

结果分析:

  • 1.首次立刻触发的KVO没有调用automaticallyNotifiesObserversForKey: 而值改变之后有调用automaticallyNotifiesObserversForKey:,不过此时返回NO,所以没能触发KVO
  • 2.options指定NSKeyValueObservingOptionInitial首次触发的KVO通知,是无法被automaticallyNotifiesObserversForKey:阻止的

接着将options指定 NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld| NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior 同时Person允许自动KVO

运行打印:

2019-12-26 15:49:26.326606+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.326725+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.326886+0800 KVC&KVO[3435:181969] change  = {
    kind = 1;
    new = 0;
}
2019-12-26 15:49:26.326981+0800 KVC&KVO[3435:181969] +[Person automaticallyNotifiesObserversForKey:]
2019-12-26 15:49:26.327225+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.327311+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.327438+0800 KVC&KVO[3435:181969] change  = {
    kind = 1;
    notificationIsPrior = 1;
    old = 0;
}
2019-12-26 15:49:26.327528+0800 KVC&KVO[3435:181969] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 15:49:26.327608+0800 KVC&KVO[3435:181969] keyPath = age
2019-12-26 15:49:26.327884+0800 KVC&KVO[3435:181969] change  = {
    kind = 1;
    new = 300;
    old = 0;
}

结果分析:

  • .由于option添加了NSKeyValueObservingOptionPrior,因此在值修改前和修改后都会触发KVO通知
注意事项:
  • 1.NSKeyValueObservingOptions指定NSKeyValueObservingOptionInitial,则一旦添加监听立刻触发KVO,无法被automaticallyNotifiesObserversForKey:阻止,并且回调接口字典change中并不包含old值
  • 2.NSKeyValueObservingOptions指定NSKeyValueObservingOptionPrior,则属性改变之前(具体在willChangeValueForKey会触发)就会被通知一次,改变之后(具体在didChangeValueForKey会触发)再通知一次

context

先来官方文档怎么说的.

image.png

使用方法addObserver:forKeyPath:options:context:添加观察时,消息中的上下文指针可以包含任意数据,并且这些数据将在相应的更改通知中传递回观察者. context可以指定NULL并完全依靠键路径字符串来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题. context可以提供一种更安全,更可扩展的方法确保观察者收到的通知是发给观察者的,而不是父类对象的. 一个良好的上下文对象可以是类中唯一命名的静态变量的地址,在父类或子类中以类似方式选择的上下文一般不会重复.可以为整个类选择一个上下文,然后依靠通知消息中的关键路径字符串来确定更改的内容.此外,也可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析.

首先我们不使用context,对单个对象简单场景(例如上面的例子),貌似没发现什么不妥~ 但是一旦稍微有点复杂,不使用context那么问题就很明显了!

问题1: 同一个类的不同对象,需要添加观察它们的age属性,怎么处理? 由于要观察的属性都是age,也就是keyPath相同,但是Object对象不同,那么你就不能依靠keyPath来区分通知来源了.此时你很容易想到使用Object来区分,这样做也确实可以. 例子如下:

#import "ViewController.h"

@interface Person : NSObject

@property (nonatomic, assign) NSUInteger age;

@end

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    NSLog(@"%s",__func__);
    return [super automaticallyNotifiesObserversForKey:key];
}

@end

@interface ViewController ()

@property (nonatomic, strong) Person * person,*person2;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
     _person = [[Person alloc] init];
     _person2 = [[Person alloc] init];
    //添加属性观察
    [_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
    
    [_person2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
    //触发KVO
    [_person setValue:@30 forKey:@"age"];
    [_person2 setValue:@40 forKey:@"age"];
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if(object == _person){
        NSLog(@"%s",__func__);
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"change  = %@",change);
         //接着让_person去处理一些事
        NSLog(@"已收到_person通知,让_person对象去做些事");
    }
    else if(object == _person2){
        
        NSLog(@"%s",__func__);
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"change  = %@",change);
        //接着让_person2去处理一些事
        NSLog(@"已收到_person2通知,让_person2对象去做些事");
    }else{
         
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
    
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
    //移除
    [_person removeObserver:self forKeyPath:@"age"];
    [_person2 removeObserver:self forKeyPath:@"age"];
}

@end
2019-12-26 16:22:41.245756+0800 KVC&KVO[3546:194556] +[Person automaticallyNotifiesObserversForKey:]
2019-12-26 16:22:41.246065+0800 KVC&KVO[3546:194556] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 16:22:41.246162+0800 KVC&KVO[3546:194556] keyPath = age
2019-12-26 16:22:41.246317+0800 KVC&KVO[3546:194556] change  = {
    kind = 1;
    new = 30;
    old = 0;
}
2019-12-26 16:22:41.246411+0800 KVC&KVO[3546:194556] 已收到_person通知,让_person对象去做些事
2019-12-26 16:22:41.246512+0800 KVC&KVO[3546:194556] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-26 16:22:41.246601+0800 KVC&KVO[3546:194556] keyPath = age
2019-12-26 16:22:41.246711+0800 KVC&KVO[3546:194556] change  = {
    kind = 1;
    new = 40;
    old = 0;
}
2019-12-26 16:22:41.246797+0800 KVC&KVO[3546:194556] 已收到_person2通知,让_person2对象去做些事

通过object对象来区分对象通知来源(包括父类子类添加相同的属性观察,也都可以,但是不建议这样做,因为可扩展性差,不够安全),简单场景确实行得通,因为这里也不够复杂.说到这里,那我们就来点复杂的

问题:2个不同类的对象,它们的属性都不相同,暂且取其中两个属性来进行观察,怎么处理?那么现在你要做的就是怎么区分通知的来源,如果不使用context,你所想到的就是多重嵌套来判断通知来源,有点类似下面的伪代码:

if(object == 对象1){
        //p1,p2 代表对象1属性
        if ([keyPath isEqualToString:@"p1"]) {
             //....
        }
        else if ([keyPath isEqualToString:@"p2"]) {
             //....
        }
}else if(object == 对象2){
         //pp1,pp2 代表对象2属性
        if ([keyPath isEqualToString:@"pp1"]) {
            //....
        }
        else if ([keyPath isEqualToString:@"pp2"]) {
             //....
        }
}

看到上面的代码你有没有点抓狂的感觉~~~ 这样的代码,容易出错(一旦判断出错,错误的通知观察者,就可能造成让程序crash),而且扩展性不强(比如:我又更改需求了,现在变为3个对象,3个不同属性需要观察,你如何处理?)

接下来我们使用context上下文参数就可以解决上述所有问题

#import "ViewController.h"

@interface Person : NSObject

@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) float height;
-(void)read;

@end

@implementation Person

-(void)read{
    
    NSLog(@"人会阅读书籍~~~");
}
@end


@interface Dog : NSObject

@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, assign) float height;

-(void)run;

@end

@implementation Dog
-(void)run{
    
    NSLog(@"狗狗会奔跑~~~");
}
@end


@interface ViewController ()

@property (nonatomic, strong) Person * person;
@property (nonatomic, strong) Dog * dog;

@end

@implementation ViewController

static void *PersonContext1 = &PersonContext1;
static void *PersonContext2 = &PersonContext2;
static void *DogContext1    = &DogContext1;
static void *DogContext2    = &DogContext2;

- (void)viewDidLoad {
    [super viewDidLoad];
     _person = [[Person alloc] init];
     _dog    =  [[Dog alloc] init];
    //添加属性观察
    [_person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:PersonContext1];
    [_person addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:PersonContext2];
    
    [_dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:DogContext1];
    [_dog addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:DogContext2];
    
    //触发KVO
    _person.age = 20;
    _person.height = 175;
    _dog.age = 2;
    _dog.height = 50;
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    
    if (context == PersonContext1) {
         NSLog(@"object = %@",object);
         NSLog(@"keyPath = %@",keyPath);
         NSLog(@"change  = %@",change);
         [_person read];
    }
    else if (context == PersonContext2) {
        NSLog(@"object = %@",object);
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"change  = %@",change);
        [_person read];
    }
    else if (context == DogContext1) {
        NSLog(@"object = %@",object);
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"change  = %@",change);
        [_dog run];
    }
    else if (context == DogContext2) {
        
        NSLog(@"object = %@",object);
        NSLog(@"keyPath = %@",keyPath);
        NSLog(@"change  = %@",change);
        [_dog run];
    }
    else{
         
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
    
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
    //移除
    [_person removeObserver:self forKeyPath:@"age" context:PersonContext1];
    [_person removeObserver:self forKeyPath:@"height" context:PersonContext2];
    [_dog removeObserver:self forKeyPath:@"age" context:DogContext1];
    [_dog removeObserver:self forKeyPath:@"height" context:DogContext2];
}

@end

打印结果如下:

2019-12-26 17:02:02.856237+0800 KVC&KVO[3692:209619] object = <Person: 0x6000000836a0>
2019-12-26 17:02:08.225155+0800 KVC&KVO[3692:209619] keyPath = age
2019-12-26 17:02:08.225185+0800 KVC&KVO[3692:209731] XPC connection interrupted
2019-12-26 17:02:09.256309+0800 KVC&KVO[3692:209619] change  = {
    kind = 1;
    new = 20;
    old = 0;
}
2019-12-26 17:02:14.048296+0800 KVC&KVO[3692:209619] 人会阅读书籍~~~
2019-12-26 17:02:18.024598+0800 KVC&KVO[3692:209619] object = <Person: 0x6000000836a0>
2019-12-26 17:02:19.599899+0800 KVC&KVO[3692:209619] keyPath = height
2019-12-26 17:02:20.167343+0800 KVC&KVO[3692:209619] change  = {
    kind = 1;
    new = 175;
    old = 0;
}
2019-12-26 17:02:20.927309+0800 KVC&KVO[3692:209619] 人会阅读书籍~~~
2019-12-26 17:02:35.256251+0800 KVC&KVO[3692:209619] object = <Dog: 0x600000083760>
2019-12-26 17:02:36.102929+0800 KVC&KVO[3692:209619] keyPath = age
2019-12-26 17:02:37.063650+0800 KVC&KVO[3692:209619] change  = {
    kind = 1;
    new = 2;
    old = 0;
}
2019-12-26 17:02:37.790867+0800 KVC&KVO[3692:209619] 狗狗会奔跑~~~
2019-12-26 17:02:43.469688+0800 KVC&KVO[3692:209619] object = <Dog: 0x600000083760>
2019-12-26 17:02:43.469830+0800 KVC&KVO[3692:209619] keyPath = height
2019-12-26 17:02:43.469978+0800 KVC&KVO[3692:209619] change  = {
    kind = 1;
    new = 50;
    old = 0;
}
2019-12-26 17:02:45.048043+0800 KVC&KVO[3692:209619] 狗狗会奔跑~~~

结果分析:

  • 1.不用context很容易出错,可能会导致错误的行为,比如 让一条狗去读书~~~
  • 2 .使用context好处不言而喻,确实更加安全,扩展性强!

禁用自动KVO

  • 注意option参数指定NSKeyValueObservingOptionInitial触发的KVO是无法被automaticallyNotifiesObserversForKey:禁用的,所以我们能做的就是:只能对除了option指定NSKeyValueObservingOptionInitial之外的触发KVO方式进行禁用

  • 禁用方式就是被观察类重写automaticallyNotifiesObserversForKey:并返回NO即可(这样会禁用所有除了option参数指定NSKeyValueObservingOptionInitial以外的自动KVO)

  • 单独禁用某个key触发的自动KVO可以采用如下两种方式: 单独提供:automaticallyNotifiesObserversOfKey并返回NO即可

+ (BOOL)automaticallyNotifiesObserversOfAge{
     return NO;
}

或者

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
   //这里age是你要禁用的key
    if([key isEqualToString:@"age"]){
        NSLog(@"对%@手动KVO",key);
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
  • automaticallyNotifiesObserversForKey:和automaticallyNotifiesObserversOfKey两者可以共存,两个方法同时存在时,优先走automaticallyNotifiesObserversForKey:方法 然后再走automaticallyNotifiesObserversOfKey,但是最后是否允许自动KVO由automaticallyNotifiesObserversForKey:决定

触发KVO的几种方式

  • 1.常规setter方法

  • 2.KVC

  • 3.消息发送,调用setter方法(注意setter不存在情况,需要动态处理)

  • 4.手动KVO

1.手动KVO首先需要注意,option参数不能指定NSKeyValueObservingOptionInitial

2.禁用自动KVO(可以参考之前如何禁用自动KVO)

3.赋值前后分别加入willChangeValueForKey:和didChangeValueForKey:方法即可(比较好的做法是在setter方法中)

    [_person willChangeValueForKey:@"age"];
    [_person setValue:@300 forKey:@"age"];
    [_person didChangeValueForKey:@"age"];
  • 5.依赖触发KVO

    有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记.最简单的例子就是一个人的姓名fullName是由firstName和lastName组成,当firstName或者lastName发生改变的时候,fullName也会跟着改变.所以如果一个观察者对fullName进行观察,那么当firstName或者lastName改变时,这个观察者也应该被通知.

根据官方文档描述有如下两种解决方案:

方案1 :重写keyPathsForValuesAffectingValueForKey:来指明fullName是依赖lastName和firstName的

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

方案2: 实现一个遵循命名方式为keyPathsForValuesAffecting的类方法,是依赖于其他值的属性名

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

例子如下:

#import "ViewController.h"

@interface Person : NSObject

@property (nonatomic, copy) NSString* fullName,*lastName,*firstName;

@end

@implementation Person

- (NSString *)fullName {
    
    NSLog(@"%s",__func__);
    return [NSString stringWithFormat:@"%@ %@",_firstName, _lastName];
}
+ (NSSet *)keyPathsForValuesAffectingFullName {
     NSLog(@"%s",__func__);
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

    NSLog(@"%s",__func__);
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

@end



@interface ViewController ()

@property (nonatomic, strong) Person *  person;

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [[Person alloc] init];
    //添加属性观察
    [_person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    _person.firstName = @"Jay";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
   
    NSLog(@"%s",__func__);
    NSLog(@"keyPath = %@",keyPath);
    NSLog(@"change  = %@",change);
    

    
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
    //移除
    [_person removeObserver:self forKeyPath:@"fullName"];
}

@end
2019-12-27 17:27:35.457517+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457636+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingFullName]
2019-12-27 17:27:35.457752+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457858+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.457946+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.458045+0800 KVC&KVO[3492:220907] +[Person keyPathsForValuesAffectingValueForKey:]
2019-12-27 17:27:35.458427+0800 KVC&KVO[3492:220907] -[Person fullName]
2019-12-27 17:27:35.458532+0800 KVC&KVO[3492:220907] -[Person fullName]
2019-12-27 17:27:35.458627+0800 KVC&KVO[3492:220907] -[ViewController observeValueForKeyPath:ofObject:change:context:]
2019-12-27 17:27:35.458709+0800 KVC&KVO[3492:220907] keyPath = fullName
2019-12-27 17:27:35.458854+0800 KVC&KVO[3492:220907] change  = {
    kind = 1;
    new = "Jay (null)";
    old = "(null) (null)";
}

结果分析:

  • keyPathsForValuesAffectingValueForKey:和keyPathsForValuesAffectingFullName 可以同时存在,两者也均会调用,但是最终结果会以keyPathsForValuesAffectingValueForKey:为准

上面都是一对一简单场景,在一对多关系中(数组属性),上述解决方案就不管用了.比如: 假如有一个部门,里面有很多员工,每个员工都有各自的薪水,现在要求统计这个部门所有员工的薪水总和. 这种情况不能通过实现keyPathsForValuesAffectingTotalSalary方法并返回employees.salary

有两种解决方法可供参考:

  • 方法1:可以使用键值观察将父类(在此示例中为Department)注册为所有子类(在此示例中为Employees)的相关属性的观察者。必须在把child添加或删除到parent时也把parent作为child的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:方法中,将更新依赖值以响应更改,如以下代码片段所示:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}
  • 方法2:如果使用的是Core Data,则可以在应用程序的通知中心将父级注册为其托管对象上下文的观察者。 父类应以类似于观察键值的方式响应子类发送的相关变更通知

看看例子:

#import "ViewController.h"

@class Department;

@interface Employee : NSObject

@property (nonatomic, assign) float salary;

@end

@implementation Employee

@end


@interface Department : NSObject


@property (nonatomic, strong) NSArray<Employee*> * employees;

@property (nonatomic, strong) NSNumber *totalSalary;

@end


@implementation Department


@synthesize totalSalary = _totalSalary;

static void *totalSalaryContext = &totalSalaryContext;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _employees = @[];
        [self addObserver:self forKeyPath:@"employees" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:totalSalaryContext];
        
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"%s",__func__);
    NSLog(@"keyPath = %@",keyPath);
    NSLog(@"change  = %@",change);
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else{
        
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (void)updateTotalSalary {
    
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (_totalSalary != newTotalSalary) {
        _totalSalary = newTotalSalary;
    }
}
 
- (NSNumber *)totalSalary {
    
    return _totalSalary;
}
- (void)dealloc
{
    NSLog(@"%s",__func__);
    [self removeObserver:self forKeyPath:@"employees" context:totalSalaryContext];
}
@end


@interface ViewController ()

@end

@implementation ViewController


- (void)viewDidLoad {
    [super viewDidLoad];
     Department *department = [[Department alloc] init];
    NSMutableArray* salaries = [department mutableArrayValueForKeyPath:@"employees"];
    for (int i =0 ; i< 5; i++) {
        Employee* emp = [[Employee alloc] init];
        emp.salary = 5000+ i*100;
        [salaries addObject:emp];
        
    }

    [salaries removeAllObjects];
    
}
@end

KVO的使用注意事项

  • 1.移除一个尚未注册的观察者将导致NSRangeException.可以对removeObserver:forKeyPath:context:和 addObserver:forKeyPath:options:context:的调用放在在try / catch块内处理潜在的异常
  • 2.观察者被释放后,观察者不会自动删除自己。被观察对象仍然会继续发送通知,而忽略了观察者的状态,则会造成内存访问异常。需要确保观察者在从内存中消失之前将自己删除!
  • 3.确保成对和有序地添加和删除观察,并且确保观察者在注册之前先未注册,移除之前未被移除.一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销
  • 4.被观察属性是集合属性(例如:NSArray)时,add和move操作不会触发KVO(例如被观察是NSArray属性,可以结合mutableArrayValueForKeyPath之后来进行add 或者remove来触发KVO)

KVO的使用场景

    1. iOS MVC架构模式 (比如:M和C之间通信,监听模型属性实时更新UI)
    1. macOS X Cocoa Bindings 技术
    1. 定制UI 比如:监听 content offset实现上下拉刷新控件(MJRefresh框架) ,监听content size实现webview混合排版等