KVO使用及原理简述

介绍

工程中我们常常需要得到成员变量属性的值的改变, 在iOS开发中:

  • 成员变量属性指对象的参数, 如: 一个人的名字: person.name

  • 成员变量或属性成员变量或属性指对象的参数的参数, 如: 一个人的孩子的名字: person.child.name

    如我们需要实时得到某个用户的信用情况, 针对不同的信用等级, 我们有不同的操作. 我们定个属性: user.credit:

  • user.credit == great, 圣诞节到了, 我们给他送个礼物

  • user.credit == good, 我们提升这个用户的信用额度

  • user.credit == ok, 我们给他打个标签: 优质用户

  • user.credit == bad, 我们关闭他的借款权限

在上述情况下, 我们可以使用Cocoa提供给我们的KVO(Key-value observing)来实现:

Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO也体现了在iOS开发中常使用的一种设计模式 - 观察者设计模式.

KVO 的使用

步骤

  1. 添加监听: addObserver: forKeyPath: options: context:
  2. 实现监听方法: observeValueForKeyPath: ofObject: change: context:
  3. 移除监听: removeObserver: forKeyPath:

示例

  1. ViewController创建一个属性
@property (nonatomic, copy) NSString *name;
  1. 添加key-value-observer
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  1. 实现监听值(此处为name)变化时的监听方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"name = %@", self.name);
    }
}
  1. ViewControllerdealloc中移除
- (void)dealloc{
    [self removeObserver:self forKeyPath:@"name"];
}
  • 注: 移除observer视实际情况而定, 也可以在viewDidDisappear:或者处理完监听, 在observeValueForKeyPath: ofObject: change: context:最后.

测试

  1. viewController中添加一个title更改name 的按钮, 为其添加一个事件, 用来修改name, 如下:
static NSInteger idx;
///修改name
- (IBAction)modifyNameAction:(id)sender {
    
    NSArray *nameArr = @[@"张三", @"李四", @"王五", @"赵六", @"Jim七", @"David八", @"Kevin九", @"Danny十"];
    self.name = nameArr[idx];
    idx++;
    if (idx > 9) {
        idx = 0;
    }
    
}
  • 注: 此处为了使代码紧凑, 未优化nameArr
  1. 点击按钮, 更改name属性, 可以看到KVO的监听方法被触发:
    KVO使用及原理简述_第1张图片
    KVO触发.gif
以上即为KVO的基本使用, 也是系统的自动调用. KVO自动调用的原理为:
  1. 系统会重写被监听属性的setter方法, 如上述的setName:, 所以, 必须监听属性, 有setter方法
  2. 系统会依次调用:
  • 1)- willChangeValueForKey:
  • 2)setter方法
  • 3)- didChangeValueForKey:
  • 4)通知观察者.
    这也解释了NSKeyValueObservingOptionOld(旧值)NSKeyValueObservingOptionNew(新值)的来源.

验证:

重写setter, willChangeValueForKey:, didChangeValueForKey:

- (void)setName:(NSString *)name{
    NSLog(@"22---setter");
    _name = name;
}

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"11---will key = %@", key);
}

- (void)didChangeValueForKey:(NSString *)key{
    [super didChangeValueForKey:key];
    NSLog(@"33--- did key = %@", key);
    
}

观察打印如下:


KVO使用及原理简述_第2张图片
方法调用顺序.gif
  • 有自动调用, 就有手动调用, 手动调用我们将在后面讲述.

监听一个属性, 实现监听多个属性

我们使用间接属性来举例

  1. 定义一个Child类, 它有4个属性: birthday, year, month, day:
///生日
@property (nonatomic, copy) NSString *birthday;
///生日的年
@property (nonatomic, assign) NSInteger year;
///生日的月
@property (nonatomic, assign) NSInteger month;
///生日的日
@property (nonatomic, assign) NSInteger day;
  1. Child.m 中, 初始化上述属性:
- (instancetype)init{
    if (self = [super init]) {
        self.birthday = @"2000-01-01";
        self.year = 2000;
        self.month = 1;
        self.day = 1;
    }
    return self;
}
  1. 定义一个Worker类, 它有一个Child属性:
@property (nonatomic, strong) Child *child;
  1. viewController 类中添加一个worker属性:
@property (nonatomic, strong) Worker *worker;
  1. 监听worker 的child 中birthday 的改变:
[self.worker addObserver:self forKeyPath:@"child.birthday" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
  1. 添加更改间接属性的事件:
//更改间接属性值事件
- (IBAction)modifyObjectAction:(id)sender {
    self.worker.child.birthday = @"2001-12-31";
}

这样在监听方法中, 我们便能得到worker.child.birthday 更改前后的值:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
   
    if ([keyPath isEqualToString:@"child.birthday"]) {
        NSLog(@“change info = %@", change);
    }
}
KVO使用及原理简述_第3张图片
监听间接属性`child.birthday`.gif
  • 上例中, 对于Child来说, 其属性birthday是由year, month, day影响的, 即当year, month, day其一改变时, 关心birthday的外界也需要收到监听. 这种情况下, 当Childyear, month 或 day改变时, 应当告诉birthday的监听者.
  • 这里就需要实现 KVO 的这个方法:
    + (NSSet *)keyPathsForValuesAffectingKey
  • 在我们重写这个方法, 系统自动补全提示时, 会将Key替换成我们的属性名称. 如此处重写的方法为:
///当kvo当前对象的birthday属性时,如果year,month,day的值发生变化,都会触发这个KVO
+ (NSSet *)keyPathsForValuesAffectingBirthday{
    return [NSSet setWithObjects:NSStringFromSelector(@selector(year)),
            NSStringFromSelector(@selector(month)),
            NSStringFromSelector(@selector(day)),
            nil];
}

这样, 只要KVO监听了birthday , 当year, month, day 改变时, 也会触发监听方法.

  • 注: 这种操作, 我们在change中得到的还是birthday的值.

监听数组

实际上, 能使用KVO来监听的属性, 必须符合Key-Value Coding, 而数组并不符合.
所以, 直接监听数组属性, 用数组默认的API来操作数组时, 是不会触发监听方法的.
实现:

  1. 被监听的对象需要实现下面方法
  2. 且操作数组属性时, 也要使用下面对应的方法:
- objectInMyArrayAtIndex:

- insertObject:inMyArrayAtIndex:

- removeObjectFromMyArrayAtIndex:

- replaceObjectInMyArrayAtIndex:withObject:

同KVO的其它方法一样, 重写这些方法时, 系统也会有补全提示, 而上述中的MyArray会替换成实际的属性名称.

  1. 依然在上述例子中, 我们为worker添加一个cities属性:
@property (nonatomic, strong) NSMutableArray *cities;
  1. Worker.m中初始化:
- (instancetype)init{
    if (self = [super init]) {
        self.cities = [NSMutableArray array];
    }
    return self;
}
  1. 实现KVO数组相关的方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index{
    return [self.cities objectAtIndex:index];
}

- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index{
    [self.cities insertObject:object atIndex:index];
}

- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index{
    [self.cities removeObjectAtIndex:index];
}

- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object{
    [self.cities replaceObjectAtIndex:index withObject:object];
}

- (void)addCitiesObject:(NSString *)object{
    [self.cities addObject:object];
}
  1. 并在Worker.h文件中公开上述方法:
- (id)objectInCitiesAtIndex:(NSUInteger)index;

- (void)insertObject:(NSString *)object inCitiesAtIndex:(NSUInteger)index;

- (void)removeObjectFromCitiesAtIndex:(NSUInteger)index;

- (void)replaceObjectInCitiesAtIndex:(NSUInteger)index withObject:(id)object;

- (void)addCitiesObject:(NSString *)object;
  1. viewController中监听:
    [self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
  1. 依次执行下面方法:
    [self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
    [self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
    [self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];
    [self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];

过滤掉无用信息后, 对应打印结果如下:

//1. [self.worker insertObject:@"nanjing" inCitiesAtIndex:0];
 change info = {
    kind = 2;
    new =     (
        nanjing
    );
}
 //2. [self.worker insertObject:@"suzhou" inCitiesAtIndex:1];
change info = {
    kind = 2;
    new =     (
        suzhou
    );
}
 //3. [self.worker replaceObjectInCitiesAtIndex:1 withObject:@"wuxi"];

change info = {
    kind = 4;
    new =     (
        wuxi
    );
    old =     (
        suzhou
    );
}
  //4. [self.worker removeObjectFromCitiesAtIndex:self.worker.cities.count-1];

change info = {
    kind = 3;
    old =     (
        wuxi
    );
}

因为字典change中存储的是变化的数组元素的值, 而不是整个数组的值, 所以对应步骤解析如下:

  • 1.添加.所以只有新值,没有旧值
  • 2.同上
  • 3.替换.新值替换旧值, 所以既有旧值,也有新值
  • 4.删除.只是删除旧值, 没有新值加入,所以只有旧值
    • 注:添加元素时,只能insertObject:AtIndex, 没有直接addObject:

关闭系统自动调用KVO, 改为手动调用

在很多情况下, 我们都应该关闭自动调用, 改为手动调用. 因为每次调用setter, 都会调用监听方法, 即使旧值与新值相同.

如我们要关闭属性name的自动调用
  1. 重写触发手动或自动调用的类方法, 并返回NO. 如
+ (BOOL)automaticallyNotifiesObserversOfName{
    return NO;
}

或者

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    //name之外的属性,还是由系统自动调用
    return YES;
}

是的, 正如你所料, 系统是默认返回YES

  1. name改变, 需要触发监听方法observeValueForKeyPath: ofObject: change: context:时, 手动调用-willChangeValueForKey:- didChangeValueForKey:
实现
  1. 把我们的名字数组的李四变成张三, 这样我们就有两个张三了:

    两个`张三.png

  2. 重写setter方法:

- (void)setName:(NSString *)name{
    
    if (![_name isEqualToString:name]) {
        
        [self willChangeValueForKey:@"name"];
        NSLog(@"22---setter");
        _name = [name copy];
        
        [self didChangeValueForKey:@"name"];
    }
}
  1. 打印如下:


    KVO使用及原理简述_第4张图片
    手动调用`KVO`.gif
利用上述KVO手动调用的原理, 我们可以监听成员变量. 步骤:

1.添加一个成员变量:

{
    int _age;
}

2.监听:

    [self addObserver:self forKeyPath:@"_age" options:NSKeyValueObservingOptionNew context:nil];

3.实现监听方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    
    if ([keyPath isEqualToString:@"name"]) {
        
        NSLog(@"kvo name = %@", self.name);
        
    }else if ([keyPath isEqualToString:@"_age"]){
        NSLog(@"age = %zd", _age);
    }
    
}

4.添加一个修改_age的事件

///修改age
- (IBAction)modifyAgeAction:(id)sender {
    
    [self willChangeValueForKey:@"_age"];
    
    _age++;
    
    [self didChangeValueForKey:@"_age"];
}

5.打印如下:


KVO使用及原理简述_第5张图片
利用手动调用`KVO`,实现监听.gif

context参数

最后我们再来看下addObserver: forKeyPath:options:context:context参数.它是监听的唯一标识,它会被代入监听方法中:observeValueForKeyPath: ofObject: change: context:
通常情况下, 我们不需要 context 参数来区别我们的监听, 但是在下面的小概率事件时:

  • 继承
  • 父类使用了KVO

就需要用到了.

  • 如上述的viewController继承自BaseViewController
  • BaseViewController也使用到了KVO.
    此时在viewController中的方法observeValueForKeyPath: ofObject: change: context:就覆盖了父类的实现.
    解决方法是:
  • 定义一个唯一的context, 如:
static void *ViewControllerContext = &ViewControllerContext;
  • 监听时,传入context:
    [self.worker addObserver:self forKeyPath:NSStringFromSelector(@selector(cities)) options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:ViewControllerContext];
  • 在监听方法中,根据context判断:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
   
    if (context == ViewControllerContext) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(cities))]) {
            NSLog(@"change info = %@", change);
        }
       
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
  • 如果使用了手动KVO, 也要注意调用super
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

以上就是我对KVO的总结, 如发现有欠妥之处, 请随时指出, 帮助我进步, 谢谢.

你可能感兴趣的:(KVO使用及原理简述)