KVO学习小结

KVO概述

KVO,或者key-value observing,是可以对OC对象的属性进行观察,并在属性发生改变的的时候,发出通知的神奇魔法。之所以称之为神奇,是因为KVO是依赖Object-C运行时机制,而我们需要处理很少的工作,就可以发送和接受通知。

KVO的使用

KVO的使用步骤

1. 注册观察者,为观察者者添加注册的key
    - (void)addObserver:(NSObject *)observer
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(nullable void *)contex;
  • observer : 观察者

  • keyPath :需要观察的属性

  • option :需要观察的属性值的变化的选择项。

    • NSKeyValueObservingOptionInitial : 如果需要在观察者注册了监听后立刻发送消息给observer,可以添加这个选择项。

    • NSKeyValueObservingOptionNew: 在回调中,获取属性改变之后的值。

    • NSKeyValueObservingOptionOld : 在回调方法中,获取属性改变之前的值。

  • context :可以认为是在回调方法中用来区分不同通知的来源的标识。

2. 实现KVO的回调方法
   //监听的回调
    - (void)observeValueForKeyPath:(NSString *)keyPath
     ofObject:(id)object
     change:(NSDictionary<NSKeyValueChangeKey,id> *)change
     context:(void *)context;
  • keyPath: 被观察的属性值

  • object:被 观察的属性值所在的对象

  • change: 是一个字典值,可以在这个对象中取出改变前后的值

    • NSKeyValueChangeNewKey

    • NSKeyValueChangeOldKey

  • context: 用来区分不同通知的来源的标识,与添加观察者方法的context搭配使用,是同一个值。

3. 移除观察者
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
  • observer: 观察者

  • keyPath: 被观察的属性值

4. 代码
    //定义 context
    static void * M2_KVONameContext = &M2_KVONameContext;

    //1、添加观察者
    - (void)addObserver{
       //添加观察者,使用context标识
       [self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld |  NSKeyValueObservingOptionNew  context:M2_KVONameContext];
    }
    
    //2、监听的回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
       if (context == M2_KVONameContext){
         if ([keyPath isEqual:@"name"]) {
           NSLog(@"NSKeyValueObservering new is %@", change[NSKeyValueChangeNewKey]);
           NSLog(@"NSKeyValueObservering old is %@", change[NSKeyValueChangeOldKey]);
         }
       }
    }
    
    - (void)dealloc{
       //3、移除
       [self.m2 removeObserver:self forKeyPath:@"name" context:M2_KVONameContext];
    }

KVO能够观察到回调的方式

  1. 属性的点语法。例:self.m2.name = @"我是测试数据";

  2. KVC方式进行赋值。例:[self.m2 setValue:@"我是测试数据" forKey:@"myName"];

  3. 直接操作成员变量的方法,是不会触发KVO回调的,这种情况下可以使用KVC的方法。

    • self.m2->pName = @"我是测试数据";这种方式不会回调KVO

    • [self.m2 setValue:@"我是测试数据" forKey:@"pName"];是可以回调KVO的

KVO禁止自动通知观察者

在程序中,一个对象的属性注册了观察者,但是不希望获取到该属性的KVO回调。比如有一些关键信息,不希望三方进行观察,就可以使用下面这个方法进行处理。

方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

代码实例:

//禁止某个属性自动的通知观察者,例如关键信息不想让三方知道,就可以重写这个函数
//该方法默认发挥true
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
   if ([key isEqual:@"name"]) {
     return  NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}

KVO手动触发

KVO的属性发生改变时,是系统自动通知给观察者。如果自己希望实现自定义传递消息的时机,可以使用下面的两个方法。

涉及的两个方法:

  • -(void)willChangeValueForKey:(NSString *)key:在属性发生改变前调用

  • - (void)didChangeValueForKey:(NSString *)key;: 在属性发生改变后调用

把属性的key值作为参数,发送NSKeyValueChangeSetting类型的通知消息,并把发送给每一个注册该key的观察者。

这两个方法的调用,必须始终成对进行。

代码实例,实现在属性值前后不一致的情况下发送消息,可以和automaticallyNotifiesObserversForKey 合作完成这个实例。

//手动触发
-(void)setName:(NSString *)name{
   if (name != _name) {
     //手动触发KVO
     [self willChangeValueForKey:@"name"];
     _name = name;
     //手动触发KVO
     [self didChangeValueForKey:@"name"];
   }
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
   if ([key isEqual:@"name"]) {
     return  NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}

可变数组的KVO通知

观察可变数组,可变数组实例化以后,添加元素的过程中,如果直接调用- (void)addObject:(ObjectType)anObject方法是不会通知KVO回调的。需要使用KVO中的方法- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key,然后再去调用addObject方法。

对象中添加爱可变数组

@interface SecondModel : NSObject
 //添加可变数组
 @property (nonatomic, strong) NSMutableArray * mutableArray;
@end

调用可变数组的添加方法

//实例化可变数组
self.m2.mutableArray = [NSMutableArray array];
//为其添加观察者
[self.m2 addObserver:self forKeyPath:@"mutableArray" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:M2_KVONameContext];
//直接调用addObject方法,不会触发KVO回调
[self.m2.mutableArray addObject:@"1"];
// 会触发KVO回调
[[self.m2 mutableArrayValueForKey:@"mutableArray"] addObject:@2];

KVC和KVO

如果对象的属性或者成员变量实现了KVO监听,使用KVC对属性或者成员变量进行赋值,都会产生KVO的通知消息,调起回调。

KVC是怎样实现的KVO回调的?下面我们添加一个成员变量myName, 并实现其settergetter方法,重写- (void)willChangeValueForKey:(NSString *)key-(void)didChangeValueForKey:(NSString *)key两个方法。在控制器中对添加myName的KVO监听,使用KVC进行赋值。

@interface SecondModel(){
   NSString * myName;
}
@end
@implementation SecondModel
//setter
- (void)setMyName:(NSString*)myName{
    NSLog(@"func is %@",NSStringFromSelector(_cmd));
    self->myName = myName;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
   NSLog(@"forUndefinedKey is %@",key);
}
//getter
- (NSString *)myName{
   NSLog(@"func is %@",NSStringFromSelector(_cmd));
   return self->myName;
}
- (id)valueForUndefinedKey:(NSString *)key{
   NSLog(@"valueForUndefinedKey is %@",key);
   return @"valueForUndefinedKey";
}
//没有找到setter/getter的情况下,是否直接访问成员变量
+ (BOOL)accessInstanceVariablesDirectly{
   return false;
}
- (void)willChangeValueForKey:(NSString *)key{
   NSLog(@"willChangeValueForKey ------ start");
   [super willChangeValueForKey:key];
   NSLog(@"willChangeValueForKey ------ end");
}
-(void)didChangeValueForKey:(NSString *)key{
   NSLog(@"willChangeValueForKey ------ start");
   [super didChangeValueForKey:key];
   NSLog(@"willChangeValueForKey ------ end");
}
@end

程序运行后,可以看到起打印结果为:

//willChangeValueForKey方法内部调用了一次getter
willChangeValueForKey ------ start
func is myName
willChangeValueForKey ------ end

//赋值 setter方法
func is setMyName:

//didChangeValueForKey方法内部调用了一次getter,并进行回调
didChangeValueForKey ------ start
func is myName
KVO 回调 : observeValueForKeyPath: ofObject: change: context:
didChangeValueForKey ------ end

可以看出,使用KVC对成员变量进行赋值的时候,是调用了以下两个方法的:

  • -(void)willChangeValueForKey:(NSString *)key:在属性发生改变前调用

  • - (void)didChangeValueForKey:(NSString *)key;: 在属性发生改变后调用

如果在工程中,不希望别人通过使用KVC对属性进行赋值,来监听属性变化的话,可以使用KVC的方法+ (BOOL)accessInstanceVariablesDirectly。这是因为在KVC中,查询不到属性的setter/getter方法的情况下,会查询该方法的返回值。如果该方法返回为true,会直接去操作成员变量,对成员变量进行赋值或者取值;如果该方法返回为false的话,就不会直接操作成员变量,而是抛出异常。所以如果重写了这个方法,并且针对key的返回值是false,在willChangeValueForKey 中调用getter方法是,就会抛出异常,别人也就无法实现属性的监听。

KVO原理探究

最近一直在学习KVO的内容,所以也搜索了很多相关的文章,大牛们写的都很详细,也很清楚。本菜鸟是站在巨人的肩膀上看世界,大牛们高屋建瓴,我也是看了挺长时间,大牛们是摸着石头过河,我是摸着大牛们过河,也算是基本对于KVO的实现原理有了理解。

简单概括KVO的实现:

KVO的实现主要依靠Runtime技术,当一个对象的属性添加了观察者(observer)以后,系统会调用Runtime的相关方法,创建出一个中间类,这个类的类名以NSKVONotifying_开头,并且继承于添加了观察者的属性所在的对象的类。并且在中间类中,重写了属性的setter方法。在重写的setter中,会调用父类的setter,并且在调用父类setter方法之前和之后,添加了通知该属性变化的方法。于此同时,重写了中间类的- (Class)class方法,使得该方法的返回值仍然是原类。也会重写- (void)dealloc,以完成相关的销毁工作。完成以上步骤后,修改原对象的isa指针,使其指向这个中间类,这样操作以后,原来的对象就变成了中间类的实例对象。

验证以上说明,对添加KVO观察前后的对象进行打印,并打印其属性列表和方法列表,添加一下代码:

- (void)desObject{
   //类名
   NSString *classMethodName = NSStringFromClass([self.m2 class]);
   NSString * objc_Runtime_Method_Name = NSStringFromClass(object_getClass(self.m2));

   //成员变量列表
   NSMutableArray *ivarStringList = [NSMutableArray array];
   unsigned int invarsCount = 0;
   Ivar *ivarList = class_copyIvarList(object_getClass(self.m2), &invarsCount);
   for (int j=0; j<invarsCount; j++) {
     Ivar i = ivarList[j];
     const char *iName = ivar_getName(i);
     [ivarStringList addObject:[NSString stringWithUTF8String:iName]];
   }

  //方法列表
   NSMutableArray *methodStringList = [NSMutableArray array];
   unsigned int count = 0;
   Method *methodList = class_copyMethodList(object_getClass(self.m2), &count);
   for (int i=0; i<count; i++) {
     Method m = methodList[i];
     SEL selector = method_getName(m);
     [methodStringList addObject:NSStringFromSelector(selector)];
   }

   //打印
   NSLog(@"classMethodName is %@  |--| objc_Runtime_Method_Name is %@",classMethodName,objc_Runtime_Method_Name);
   NSLog(@"ivarList is %@",ivarStringList);
   NSLog(@"methodStringList is %@",methodStringList);
   NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}

并在添加KVO前后添加该方法:

 [self desObject];
 [self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionOld |  NSKeyValueObservingOptionNew  context:M2_KVONameContext];
 [self desObject];

运行程序后,得到以下的信息:

classMethodName is SecondModel  |--| objc_Runtime_Method_Name is SecondModel
ivarList is (
 "_name"
)
methodStringList is (
 name,
 "setName:",
 ".cxx_destruct"
)
~~~~~~~~~~~~~~~~~^^添加KVO之前^^~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~vv添加KVO之后vv~~~~~~~~~~~~~~~~~~~~
classMethodName is SecondModel  |--| objc_Runtime_Method_Name is NSKVONotifying_SecondModel
ivarList is (
)
methodStringList is (
 "setName:",
 class,
 dealloc,
 "_isKVOA"
)

又以上的打印结果可知:

  1. 使用[self.m2 class]打印的类名是SecondModel,而使用object_getClass(self.m2) 打印的类名是NSKVONotifying_SecondModel

  2. 中间类的方法列表中有class方法,说明中间类是重写了这个方法,并且重写后,该方法的返回值是原类。

  3. 中间类的方法列表中有setName:方法,说明中间类也重写了这个方法。

KVO的自定义实现

研究了这两篇文章:KVO原理分析runtime模拟实现KVO监听机制,也按照二位大神的方式,自己敲代码,把block形式的KVO进行了实现。在研究期间,对于KVO的实现原理理解的更清楚。有想要深刻了解的同学,可以看看这两篇文章,大有裨益。我这里对block形式的KVO总结了一个流程图,展示如下:

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

推荐阅读更多精彩内容