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<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的总结, 如发现有欠妥之处, 请随时指出, 帮助我进步, 谢谢.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 本文结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等开会阅读 1,635评论 1 21
  • 本文由我们团队的 纠结伦 童鞋撰写。 文章结构如下: Why? (为什么要用KVO) What? (KVO是什么...
    知识小集阅读 7,408评论 7 105
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,011评论 0 26
  • 你要知道的KVC、KVO、Delegate、Notification都在这里 转载请注明出处 http://www...
    WWWWDotPNG阅读 1,794评论 1 3