KVO原理探究+自定义KVO实现

  • 概念
  • 基本使用
  • 触发模式
  • 属性依赖
  • 容器类的使用
  • 自定义KVO

概念

KVO全称Key-Value Observing,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

基本使用

使用KVO分为三个步骤:
1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
2.在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
注册方法

/**
 注册KVO监听

 @param observer 观察者对象
 @param keyPath 需要观察的属性,由于是字符串形式,容易crash,一般利用系统的反射机制 NSStringFromSeletor(seletor(keypath))
 @param options 监听枚举类型
                OptionNew 接收新值,默认为只接收新值
                OptionOld 接收旧值
                OptionInitial在注册时立即接收一次回调,在改变时也会发送通知
                OptionPrior 改变之前发一次,改变之后发一次
 @param context 传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式,主要用于多个监听器对象监听相同keypath时进行区分
 */
-(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

tips:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的carsh.
KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

监听方法
观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。

/**
 监听回调方法

 @param keyPath 监听的属性路径
 @param object 被观察对象
 @param change 监听内容的变化,是个字典
 @param context 一个用来传值的对象,由注册方法添加
 */
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

触发模式

自动触发
控制当前对象的自动调用过程

/**
 调用模式,是否响应

 @param key KVO观察属性
 @return YES 正常发送通知; NO 不发送通知
 */
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

手动触发

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

属性依赖

A类的属性是一个拥有多个属性的对象B,要观察该对象,而不想写多个addObserver方法,
可以在A类的.m文件中重写

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
   
   NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
   
   //dog属性的相关属性发生变化, 避免因为监听dog的多个属性而写多份KVO监听代码
   if ([key isEqualToString:@"dog"]) {
       keyPaths = [NSSet setWithObjects:@"_dog.age",@"_dog.color",nil];
   }
   return keyPaths;
}

容器类的使用

    //监听容器类
    [self.person addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];
    //该方法无法触发
    [self.person.array  addObject:@"元素"];
    //使用以下方法可以触发,
    NSMutableArray *tempArray = [self.person mutableArrayValueForKey:@"array"];
    [tempArray addObject:@"元素"];

自定义KVO

KVO是通过isa-swizzling技术实现的
1.创建一个子类 NSKVONotifying_Person,继承于被观察的类
2.重写setter方法
3.外界改变isa指针
object_getClassName(p)
开始是原类: isa指向Person
p addObserver:forKeyPath:options:context:
注册监听之后:isa指向 NSKVONotifying_Person
根据这个思路,我自己写了一个自定义KVO,仅供技术交流,不能在项目中直接使用


#import "MyKVOPerson+KVO.h"
#import <objc/message.h>
@implementation MyKVOPerson (KVO)

- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    
    //创建子类
    //创建一个新的类  模仿系统类 NSKVONotifying_MyKVOPerson
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [@"MYKVONotifying_" stringByAppendingString:oldClassName];
    
    //创建类 并且注册类
    Class myClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
    objc_registerClassPair(myClass);
    
    //重写setName方法 实际是添加一个和父类同名的方法
    class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@");
    
    //外部改变isa指针指向
    object_setClass(self, myClass);
    
    //属性绑定  将观察者保存到当前对象 OBJC_ASSOCIATION_ASSIGN weak 防止循环引用
    objc_setAssociatedObject(self, @"Observer", observer, OBJC_ASSOCIATION_ASSIGN);
}

void setName(id self,SEL _cmd,NSString *newName) {
  
    NSLog(@"修改成功");
    //调用父类的setName方法,改变name的值
    Class class = [self class];
    object_setClass(self, class_getSuperclass(class));
//
    ((void(*)(id,SEL,NSString *))objc_msgSend)(self,@selector(setName:),newName);
    
    //拿到观察者
    id observer = objc_getAssociatedObject(self, @"Observer");
    if (observer) {
        //发消息
        ((void (*)(id, SEL,NSString*,id, NSDictionary<NSKeyValueChangeKey,id> *,void *))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"name":newName,@"kind":@1},nil);
    }
    
    //修改为self,改回子类
    object_setClass(self, class);
    
}

详细Dmeo请移步GitHub👉KVODemo

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