美文网首页
KVO使用及原理简述

KVO使用及原理简述

作者: cyh老崔 | 来源:发表于2017-11-10 10:39 被阅读211次

    介绍

    工程中我们常常需要得到成员变量属性的值的改变, 在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<NSKeyValueChangeKey,id> *)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触发.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);
        
    }
    

    观察打印如下:


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

          本文链接:https://www.haomeiwen.com/subject/vewvmxtx.html