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

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

KVO中API参数详解

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

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

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

options

options 有以下四中配置安全

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

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

  • 3.NSKeyValueObservingOptionInitial 注册后马上触发KVO通知架构

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

  • 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
复制代码

打印结果以下:oop

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混合排版等
相关文章
相关标签/搜索