iOS--KVO

Introduction to Key-Value Observing Programming Guide

Key-value observing 键-值观察是一种机制,可以用于监听某个对象的指定属性值在发生更改时得到通知,是Objective-C对观察者设计模式的一种实现。经常用于程序中Controller对象观察Model对象的属性,View对象通过Controller观察Model对象的属性。此外,Model对象可以观察其他Model对象(通常用于确定从属值何时更改),甚至可以观察自身(再次确定从属值何时更改)。

可以观察属性,包括简单属性、to-one关系和to-many关系。to-many关系的观察者会被告知所做更改的类型以及更改涉及的对象。

KVO的实现基础之一是被监控对象必须拥有相应的setter方法,换句话说只有ivar(成员变量)的类是无法进行监控的。
成员变量直接修改需要手动触发KVO

   [self willChangeValueForKey:@"keyPath"];
    ivar = newivar; 
    [self didChangeValueForKey:@"keyPath"];

举个例子说明KVO如何发挥作用。假设Account表示Person在银行的储蓄帐户。Person实例可能需要知道Account实例的某些方面何时发生更改,例如余额或利率。

Art/kvo_objects_properties.png

如果这些属性是Account的公共属性,Person可以定期轮询Account以发现变化,但这显然是低效的,且常常是不切实际的。更好的方法是使用KVO,它类似于在发生更改时接收中断。

要使用KVO,首先必须确保被观察的对象(本例中的Account )与KVO兼容。通常,如果对象继承自“NSObject”,并且以常规方式创建属性,那么对象及其属性将自动兼容KVO。也可以手动实现。KVO compliance描述了自动和手动实现KVO的区别,以及如何实现两者。

接下来,必须将观察者实例Person注册到被观察的实例Account。对于每个被观察到的key path,Person发送一条addObserver:forKeyPath:options:context:消息给Account,并将自己命名为观察者。

Art/kvo_objects_add.png

为了从Account接收更改通知,Person实现了observeValueForKeyPath:ofObject:change:context:方法,这个方法所有观察者都需要实现。Account 在每次注册的key paths发生更改时将此消息发送给Person。然后 Person可以根据变更通知进行适当的处理。

Art/kvo_objects_observe.png

最后,当它不再需要通知,至少在它被deallocated之前, Person实例必须通过发送removeObserver:forKeyPath:消息给Account取消注册。

Art/kvo_objects_remove.png

Registering for Key-Value Observing 描述了注册、接收和取消注册KVO通知的整个生命周期。
Registering Dependent Keys 解释了如何指定一个键的值依赖于另一个键的值。

与使用NSNotificationCenter的通知不同,KVO没有为所有观察者提供更改通知的中心对象。相反,当发生更改时,通知会直接发送到观察对象。`NSObject'提供了键值观察的基本实现,我们很少需要重写这些方法。

重要提示:

  • 并非所有类都对所有属性都兼容KVO。通过遵循KVO Compliance中描述的步骤,可以确保自己的类符合KVO。通常情况下,苹果提供的框架中的属性只有在上面文档记录的才符合KVO。

  • addObserver:forKeyPath:options:context:方法不对观察对象、被观察对象或上下文的强引用。如有必要应该确保维护对观察对象、被观察对象和上下文的强引用。

移除观察者时,记住以下几点:

  • 如果尚未注册为观察员,则请求将其作为观察员删除会导致 NSRangeExceptionaddObserver:forKeyPath:options:context:removeObserver:forKeyPath:context:要对应,或者在try/catch块内调用removeObserver:forKeyPath:context:以处理潜在异常。

  • 释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已释放对象的更改通知会触发内存访问异常。因此,要确保观察者在从内存中释放之前将自己移除。

  • 协议没有提供询问对象是观察者还是被观察者的方法。构造代码以避免与释放release相关的错误。一种典型的模式是在观察者初始化期间(例如在init或viewDidLoad中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。

KVO实现细节

1、 KVO 的实现依赖于 Objective-C 的 Runtime 。自动KVO是使用 isa-swizzling的技术实现的。isa 指针指向维护调度表的对象类。这个调度表本质上包含了指向类实现的方法和其他数据的指针。

2、 当观察者为对象K的属性注册时,runtime 动态创建一个继承自K对象的中间类NSKVONotifying_K,并将K的isa指针指向这个中间类。因此,isa指针的值不一定反映实例的实际类。不能依赖isa指针来确定类成员身份。

3、重写了被观察属性的 setter 方法。setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改。当修改实例对象的属性时秒回调用Foundation的 _NSSetXXXCalueAndNotify函数

  • willChangeValueForKey:
  • 父类K原来的 setter
  • didChangeValueForKey:,其内部会触发监听器(Oberser)的监听方法:- (void)observeValueForKeyPath: ofObject: change: context:

示例验证(这里需要引入一个分类NSObject+DLIntrospection打印instanceMethods)

//KVOModel类
@interface KVOModel : NSObject
@property(nonatomic, copy)NSString *name;
@end

//controller

_kvo1 = [KVOModel new];
    NSLog(@"1、-------监听之前");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

       
    [_kvo1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"2、-------监听之后");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

    [_kvo1 removeObserver:self forKeyPath:@"name"];
    NSLog(@"3、-------去掉监听之后");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass: %@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

运行项目

2020-09-16 16:58:46.955488+0800 TT[14496:252466] 1、-------监听之前
2020-09-16 16:58:46.955717+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.955856+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.955962+0800 TT[14496:252466] object_getClass_:KVOModel
2020-09-16 16:58:46.956310+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void).cxx_destruct",
   "- (id)name",
   "- (void)setName:(id)arg0 "
)
2020-09-16 16:58:46.956743+0800 TT[14496:252466] 2、-------监听之后
2020-09-16 16:58:46.956898+0800 TT[14496:252466] setter_地址_:0x105e0798b
2020-09-16 16:58:46.957024+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957154+0800 TT[14496:252466] object_getClass_:NSKVONotifying_KVOModel
2020-09-16 16:58:46.957337+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void)setName:(id)arg0 ",
   "- (class)class",
   "- (void)dealloc",
   "- (BOOL)_isKVOA"
)
2020-09-16 16:58:46.957484+0800 TT[14496:252466] 3、-------去掉监听之后
2020-09-16 16:58:46.957606+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.957710+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957840+0800 TT[14496:252466] object_getClass: KVOModel
2020-09-16 16:58:46.958318+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void).cxx_destruct",
   "- (id)name",
   "- (void)setName:(id)arg0 "
)

上面的结果说明,在KVOModel对象的实例 _kvo1 被观察时,runtime动态创建了一个KVOModel类的子类NSKVONotifying_KVOModel,而且为了隐藏这个行为,NSKVONotifying_KVOModel重写了- (Class)class方法返回之前的KVOModel类。但是使用object_getClass()就暴露了,因为这个方法返回的是这个对象的isa指针,isa指针指向的一定是个这个对象的类对象。

NSObject+DLIntrospection 的instanceMethods是在arc下所有dealloc调用完成后负责释放所有的变量。
从上面2、-------监听之后的打印可以看出,动态类重写了4个方法:

  • - (void)setName:(id)arg0:最主要的重写方法,set值时调用通知函数;
  • - (class)class隐藏自己,返回原来类的class;
  • - (void)dealloc清理监听时的动态修改;
  • - (BOOL)_isKVOA内部使用的标示,判断这个类有没被KVO动态生成子类。
    image.png

KVO 的缺点:

  • 只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知,在复杂的业务逻辑中,准确判断被观察者相对比较麻烦。
  • 需要手动移除观察者,且移除观察者的时机必须合适;
  • 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针。

自己代码实现KVO

系统是自动实现的中间类NSKVONotifying_KVOModel,我们自己手动创建一个中间类CustomKVO_KVOModel。给NSObject创建一个分类,让每一个对象都拥有我们自定义的KVO特性。这里只做简单实现以帮助增加对KVO原理的理解。

//NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end

//NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>
#import <objc/message.h>

@implementation NSObject (KVO)


- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
//这里自定义kvo
{
    //注册一个类
    
    //生成中间类的类名 CustomKVO_XXX
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
    
    //动态创建类
    Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //设置对象的isa指针,修改 isa 指向
    object_setClass(self, customClass);
    //重写 setter 方法
    NSString *methodName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
    SEL sel = NSSelectorFromString(methodName);
    //将方法添加到动态类
    class_addMethod(customClass, sel, (IMP)kvo_setter, "v@:@");
    
    //关联 观察者 属性
    objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN);

}

//IMP ----setter:
void kvo_setter(id self, SEL _cmd, NSString *name){
    //改变父类的属性值
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])
    };
    
    //调用父类
    objc_msgSendSuper(&superClass, _cmd, name);
    
    //获取观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
    
    //获取setter方法名
    NSString *methodName = NSStringFromSelector(_cmd);
    //settName:   获取 name
    NSString *key = getValueKey(methodName);
    
    //通知观察者变化 调用observeValueForKeyPath
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:name}, nil);
}

NSString *getValueKey(NSString *setter)
{
    //在setter方法中截取属性key 如 setName: 中截取 name,没有做容错
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *key = [setter substringWithRange:range];
    key = [key lowercaseString];
    
    return key;
}


此时我们调用自己定义的监听方法, 效果和系统的也是一样的

[_kvo1 m_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

补充

  1. 直接修改成员变量不会触发 KVO,因为没有通过 setter 方法。
  2. 通过 KVC 修改属性会触发 KVO。
  • KVC 通过 setValue: forKey:setValue: forKeyPath:赋值
  • KVC 在修改值得前后会分别自动调用willChangeValueForKey和didChangeValueForKey:
    • (BOOL)accessInstanceVariablesDirectly是否可以访问成员变量默认是 YES, 可以访问,所当根据 key找到的是成员变量时(没有setKey:和 _setKey:方法)也会触发 KVO


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