正确使用KVO的姿势

使用KVO的前提条件

  1. 该类必须满足KVC命名约定查看此处
  2. 该类可以触发属性变更的KVO通知继承自NSObject的类默认由NSObject实现该功能
  3. 依赖的属性被正确的注册到KVO。如:fullName依赖lastNamefirstName

使用方式

  1. 添加观察者:addObserver:forKeyPath:options:context:,如:需要观察Account对象的balance属性
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

[account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
  1. 观察回调方法处理:observeValueForKeyPath:ofObject:change:context:
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if (context == PersonAccountBalanceContext && [keyPath isEqualToString:@"balance"]) {
        
        NSLog(@"Do something with the balance…");
        
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}
  1. 移除观察者:removeObserver:forKeyPath:context:
[account removeObserver:self
             forKeyPath:@"balance"
                context:PersonAccountBalanceContext];

以上是我们常见的使用KVO的方式,在具体实践中还有许多坑要踩,接下来我们逐个探讨。

进阶使用

截止目前我们使用KVO的方式都是依赖系统实现的自动触发机制,在有些情况下我们需要更精确的控制KVO的触发时机,此时需要手动触发KVO。

  1. 手动触发KVO

手动触发KVO需要覆写NSObjectautomaticallyNotifiesObserversForKey:方法。该方法默认返回YES,调用自动触发KVO的逻辑。对需要手动触发KVO的属性需要变更该方法的返回值为NO。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    BOOL automatic = NO;
    
    // 手动触发balance属性KVO
    if ([key isEqualToString:@"balance"]) {
        automatic = NO;
    } else {
    
        // 其它属性KVO自动触发
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

实现手动触发KVO通知还需要在属性值变更前调用willChangeValueForKey:变更后调用didChangeValueForKey:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}

从OS X 10.5开始automaticallyNotifiesObserversForKey:会在被观察的类中查找+automaticallyNotifiesObserversOf<Key>方法,其中<key>表示被观察类的属性。以balance属性为例,实现automaticallyNotifiesObserversOfBalance即可,此时不需要再覆写automaticallyNotifiesObserversForKey:

+ (BOOL)automaticallyNotifiesObserversOfBalance {
    return NO;
}
  1. 依赖属性KVO

在许多情况下一个属性需要依赖其它属性值,例如:fullName是由firstNamelastName组成。

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}

假设我们需要对fullName做观察,当firstNamelastName有变化时自动更新fullName的值并触发KVO通知。

实现依赖属性KVO有以下两种方式。

  • 实现keyPathsForValuesAffectingValueForKey:方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
  • 实现keyPathsForValuesAffecting<Key>方法,其中<key>表示被观察对象的属性
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}
  • 注意事项:在category中依赖属性观察不能覆写keyPathsForValuesAffectingValueForKey:方法,因为category中不能覆写方法。在category中可以通过实现keyPathsForValuesAffecting<Key>方法实现依赖属性观察。

注意事项

  1. keypath使用方式优化

截止目前我们都是以字符串的方式使用keypath,这种方式使得编译器在编译期间不能及时的发现错误,一种比较好的方式是通过NSStringFromSelector@selector结合的方式来使用keypath,如下所示。

 [self addObserver:self
               forKeyPath:NSStringFromSelector(@selector(fullName))
                  options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                  context:FullNameContext];
  1. Context使用

在使用KVO时有一个关键点就是在添加观察者时传入一个唯一的context,如下所示。

// 观察fullName的context
static void *FullNameContext = &FullNameContext;
...
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
     if (context == FullNameContext && [keyPath isEqualToString:NSStringFromSelector(@selector(fullName))]) {
        // do somthing with fullName
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

这样会保证我们观察的子类都是正确的,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

安全的移除观察者

当一个观察者完成了监听对象的改变使命,需要调用–removeObserver:forKeyPath:context:来移除观察,否则会出现程序崩溃的情况。
当重复移除观察者时同样会导致程序崩溃。

目前还没有公开的API来检测一个对象是否被注册为观察者。
要安全的移除观察者可以通过以下两种方式来实现。

  1. try/catch
- (void)dealloc {
   
    @try {
        [self removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(fullName))
                     context:FullNameContext];
    } @catch (NSException * exception) {
        
    }
}
  1. 遍历observationInfo

通过查阅API我们发现NSObject有一个observationInfo属性,官方文档对该属性的描述如下。

Returns a pointer that identifies information about all of the observers that are registered with the observed object.

基于此可以通过KVC的方式获取到对象是否注册相关keypath的观察者

observationInfo结构
// 按key检索
- (BOOL)observerKeyPath:(NSString *)key observer:(id)observer
{
    id info = self.observationInfo;
    NSArray *array = [info valueForKey:@"_observances"];
    for (id objc in array) {
        id Properties = [objc valueForKeyPath:@"_property"];
        id newObserver = [objc valueForKeyPath:@"_observer"];
        
        NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
        if ([key isEqualToString:keyPath] && (newObserver == observer)) {
            return YES;
        }
    }
    return NO;
}

实现原理

KVO的实现官方文档中提到使用了isa-swizzling技术,实现思路如下

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

并未透漏更多细节内容。
关于KVO实现细节的探究可以参考KVO实现原理

优雅的使用KVO

关于KVO被吐槽最多的就是其晦涩的API和使用方式,如何解决这个问题呢,可以使用Facebook开源的KVOController,使用方式如下

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
[self.KVOController observe:clock 
                    keyPath:@"date"
                    options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew 
                    block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

        // update clock view with new value
        clockView.date = change[NSKeyValueChangeNewKey];
}];

使用KVOContoller解决了以下问题。

  • 不需要再手动移除观察者
  • 使用Block方式降低接口使用复杂度
  • 不再需要if判断keypath

参考

Key-Value Observing Programming Guide

Key-Value Observing

KVO和KVC

如何优雅地使用 KVO

KVO实现原理

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,009评论 0 26
  • 《招聘一个靠谱的 iOS》—参考答案(下) 说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:《招聘一个靠...
    YuWenHaiBo阅读 4,033评论 0 16
  • 本文结构如下: Why? (为什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等开会阅读 1,625评论 1 21
  • KVC/KVO 概念 KVC : 即 Key-Value-Coding,用于键值编码。作为 cocoa 的一个标准...
    满脸胡茬的小码农阅读 1,938评论 2 8