iOS KVO的总结

kvo是一种键值观察机制,能够通知对象,某个属性的值的发生了改变

注册为观察者

观察对象首先通过发送消息 addObserver:forKeyPath:options:context:将自身注册为观察者,这个必须得符合kvc的方式

添加观察者的方法

static void * viewContext = &viewContext;
//    这是创建一个静态变量,把自己的地址给变量
    [self.view addObserver:self forKeyPath:NSStringFromSelector(@selector(backgroundColor)) options:NSKeyValueObservingOptionInitial  context:viewContext];

选项的key

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {

    /* Whether the change dictionaries sent in notifications should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
    */
//可以在通知中,检测到新值,NSKeyValueChangeNewKey,使用change[NSKeyValueChangeNewKey];
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,

    /* Whether a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer's -observeValueForKeyPath:ofObject:change:context: method. When this option is used with -addObserver:toObjectsAtIndexes:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
    */
//在监听的对象的属性改变之前,在添加观察者后,在方法返回之前,就会调用1次通知的方法
    NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,

    /* Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is [NSNumber numberWithBool:YES], but never contains an NSKeyValueChangeNewKey entry. You can use this option when the observer's own KVO-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving an -observeValueForKeyPath:ofObject:change:context: message after the change.)

When this option is specified, the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified, except for ordered unique to-many relationships represented by NSOrderedSets.  For those, for NSKeyValueChangeInsertion and NSKeyValueChangeReplacement changes, the change dictionary for a will-change notification contains an NSKeyValueChangeIndexesKey (and NSKeyValueChangeOldKey in the case of Replacement where the NSKeyValueObservingOptionOld option was specified at registration time) which give the indexes (and objects) which *may* be changed by the operation.  The second notification, after the change, contains entries reporting what did actually change.  For NSKeyValueChangeRemoval changes, removals by index are precise.
    */
//这个key在对象中属性要发生改变的时候,就会调用一次,再发生改变后又会调用一次通知,两个方法可以根据`NSKeyValueChangeNotificationIsPriorKey`来进行区分,[change objectForKey:NSKeyValueChangeNotificationIsPriorKey]
    NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08

};

收到监听的对象的属性的改变的通知

当一个被观察属性的值发生改变时,会观察者收到observeValueForKeyPath:ofObject:change:context:的消息。所有的观察者必须实现这个方法。这个方法中的参数和注册观察者方法的参数基本相同,一个只有change不同。change是一个字典,里面它所有游戏了的信息由注册时的options决定。
官方提供了这些关键我们给取来到change中的价值:

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
//观察kind的key,返回的是一个数字值,为NSKeyValueChange类型的值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
//返回一个改变的新值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
//返回原来的旧值
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
//这个键的价值是一个NSIndexSet,包含了发生插入,删除,替换的对象的索引集合
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
//这个关键包含了一个NSNumber,里面是一个布尔值,如果在注册时options中有NSKeyValueObservingOptionPrior,那么在前一个通知中的change中就会有这个关键的价值,我们可以这样来判断是不是在改变前的通知
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    if (context == viewContext) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(backgroundColor))]) {
            //      id objc =  [change objectForKey:NSKeyValueChangeKindKey];
            NSLog(@"%@ %@",change,[change objectForKey:NSKeyValueChangeNewKey]);

        }
    }
    
    NSLog(@"%@  %@",keyPath,[change objectForKey:NSKeyValueChangeNewKey]);
}

当change[NSKeyValueChangeKindKey]是NSKeyValueChangeSetting的时候,说明被观察属性的setter方法被调用了。
而下面三种,根据官方文档的意思是,当被观察属性是集合类型,且对它进行了insert,remove,replace操作的时候会返回这三种关键,但是我自己测试的时候没有测试出来😓不知道是不是我理解错了。

  • NSKeyValueChangeNewKeyNSKeyValueChangeOldKey顾名思义,当你在注册的时候options参数中填了对应的NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld,并且NSKeyValueChangeKindKey的值是NSKeyValueChangeSetting,你就可以通过这两个键取到旧值和新值。
  • NSKeyValueChangeIndexesKey,当NSKeyValueChangeKindKey的结果是NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement的时候,这个键的价值是一个NSIndexSet,包含了发生插入,删除,替换的对象的索引集合
  • NSKeyValueChangeNotificationIsPriorKey,这个关键包含了一个NSNumber,里面是一个布尔值,如果在注册时options中有NSKeyValueObservingOptionPrior,那么在前一个通知中的change中就会有这个关键的价值,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES;

上下文 context

可以使用NULL(就是不使用上下文的情况),判断只能根据keyPath唯一来来做校验,但是这样如果父类也同样监听了相同的属性,这样就会有问题

删除观察者对象

一般在dealloc中进行删除,一般使用@try @catch来实现,因为KVO不是正式协议,不能确定观察了哪个对象的属性,移除没有观察的属性,会奔溃,所有要使用@try @catch这个方法

-(void)dealloc{
    NSLog(@"消失");
    @try {
//        放可能发生错误代码,如果发生了异常,就会跳入@catch中,如果没有异常就不会进入@catch中
        [self.view removeObserver:self forKeyPath:NSStringFromSelector(@selector(backgroundColor))];
    } @catch (NSException *exception) {
//发生了异常,可以上报异常操作
        
    } @finally {
//        不管有没有异常,都会执行这里
    }
}

kvo合规性

有两种方式,能够使发送了改变,通知被发出

  • 自动通知,继承自NSObject的,并且所有的属性符合KVC规范,并实现KVO的监听,不需要任何代码,就能自动实现
  • 手动通知,可以在子类中实现+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key来返回NO使用手动通知
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:NSStringFromSelector(@selector(dataArray))]) {
        return NO;
    }else{
        return [super automaticallyNotifiesObserversForKey:key];
    }
    return YES;
}

手动发送通知的时候的方法,在方法变化之前发送将要改变通知,在属性改变后发送已经改变完成的通知

-(void)addDataArrayObject:(NSString *)object{
    [self willChangeValueForKey:NSStringFromSelector(@selector(dataArray))];
    [self.dataArray addObject:object];
    [self didChangeValueForKey:NSStringFromSelector(@selector(dataArray))];
}

多对多的关系

    [self removeDataArrayAtIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];
-(void)removeDataArrayAtIndexes:(NSIndexSet *)indexes{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"dataArray"];
    [self.dataArray removeObjectsAtIndexes:indexes];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"dataArray"];
}

键之间的依赖关系

一个属性的值可能由好几个相关的属性构成的,其中一个属性发生了变换,这个属性就会发生改变,也应该接受到改变通知

比如一个fullName是由firstName和lastName构成的,当如果firstName发生了改变,fullName应该接受到通知


@property (nonatomic,copy) NSString * fullName;
@property (nonatomic,copy) NSString * firstName;
@property (nonatomic,copy) NSString * lastName;
-(NSString *)fullName{
    return [NSString stringWithFormat:@"%@%@",self.firstName,self.lastName];
}
- (void)viewDidLoad {
   self.firstName = @"李";
    self.lastName = @"明";
//监听fullName的值
 [self addObserver:self forKeyPath:NSStringFromSelector(@selector(fullName)) options:NSKeyValueObservingOptionNew context:nil];
//改变firstName的值
    self.firstName = @"王";
}
//这个是增加依赖关系的,这个fullName是由firstName lastName构成的,实际上就是给fullName增加两个keypath属性,第一种设置依赖关系的方式
+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
  NSSet * set =  [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:NSStringFromSelector(@selector(fullName))]) {
       set =  [set setByAddingObjectsFromArray:@[@"firstName",@"lastName"]];
    }
    return set;
}

//这个是增加依赖关系的,这个fullName是由firstName lastName构成的,第二种设置依赖关系的方式

+(NSSet<NSString *> *)keyPathsForValuesAffectingFullName{
    return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

多对多的关系


keyPathsForValuesAffectingValueForKey:方法不能支持to-many的关系。举个例子,比如你有一个部门对象,和很多个雇员对象。而雇员有一个薪属属性。你可能希望部门对象有一个totalSalary的属性,依赖于所有的雇员的工资。
你可以注册部门成为所有雇员的观察者。当雇员被添加或者被移除时,你必须要添加和移除观察者。然后在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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容