iOS:KVO

本文仅是记录自己在学习的过程中的理解:如有错误,还望各位大佬指正,THX.

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

KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

1. KVO 的基本使用

相信大家在平时的开发中都使用过KVO,使用KVO分为3个步骤:
1.通过- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注册观察者,观察者可以接收keyPath属性的变化事件。

参数部分:
--Observer参数:观察者对象
--keyPath参数:需要观察的属性,由于是字符串的形式,写错的话很容易导致崩溃,一般利用系统的反射机制NSStringFromSelector(@selector(keyPath));
--options参数:枚举类型
NSKeyValueObservingOptionNew 接收新值,默认为只接收新值
NSKeyValueObservingOptionOld 接收旧值
NSKeyValueObservingOptionInitial 在注册的时候立即接收一次回调,在改变是也会发生通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后发一次
--context参数:传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式
**注意:在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致由于观察者的释放而带来的崩溃。

2.在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,当keyPath属性发生改变之后,KVO会回调这个方法来通知观察者属性的改变。
3.当观察者不需要监听的时候,可以调用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法将KVO移除,需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致崩溃。一般在dealloc中调用。

KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

2. KVO的触发模式

KVO在属性发生改变的时候默认是自动调用的,如果需要手动的控制这个调用时机,或者自己来实现KVO属性的调用,可以通过KVO提供的方法来调用。
在所要观察的对象.m文件中加入:

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    return YES;//默认,自动模式
    return NO;//手动模式
}

同时在属性变化之前,调用:

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

在属性变化之后,调用:

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

其实无论属性的值是否发生改变,是否调用Setter方法,只要调用了willChangeValueForKey:和didChangeValueForKey:就会触发回调。

一般我们在开发的时候,需要用到KVO监听属性值得变化,一般不会将所有的值得监听都是手动的触发,同时我们也看到automaticallyNotifiesObserversForKey:传入了一个参数key, 就是为了让我们根据key来决定是否手动开启KVO.

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;//手动模式
    }
    return YES;//默认,自动模式
}
3. KVO属性依赖

如果在当前Person类中引入另外一个Dog类:

//  Dog.h
@interface Dog : NSObject
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,assign) NSInteger level;
@end

//  Person.h
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong) Dog *dog;
@end

//Person.m
@implementation Person
-(instancetype)init
{
    if (self = [super init]) {
        _dog = [[Dog alloc] init];
    }
    return self;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;//手动模式
    }
    return YES;//默认,自动模式
}

那么此时我们怎么通过Person来观察Dog类的age属性呢?

[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];

如果Dog类有多个属性;那么我们现在希望,只要Dog类中有属性的变化,就会通知到Person类,如果我们每一个属性都添加一遍观察者,是不是很麻烦,那么这里就需要用到属性依赖:我们在Person类的.m中添加一个方法:

+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPath = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqual:@"dog"]) {
        keyPath = [[NSSet alloc] initWithObjects:@"_dog.age",@"_dog.level", nil];
    }
    return keyPath;
}

同时在添加观察者时,不用对dog具体的属性添加:
 [_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
4. KVO 的原理

KVO的其实就是观察属性的变化,也就是setter方法的变化,但是上面我们也提到过就是不需要调用setter方法同样可以触发KVO,那么KVO到底是不是观察setter方法呢?现在我们把代码恢复到最初的时候,此时只观察Person类的name属性,如果此时把name改成成员变量:

//  Person.h
@interface Person : NSObject
{
 @public
    NSString *name;
}
//@property (nonatomic,copy) NSString *name;
@end

//调用改变name
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int a;
    
     _p.name = [NSString stringWithFormat:@"%d",a++];
 }

当改变name的值得时候,可以发现此时并不会有回调。
那么可以知道,其实KVO观察的还是属性的setter方法。
那么如何实现当调用Person类对象的setter方法的时候能够观察到改变呢?一般有两种方式实现:分类和子类继承
那么我们可以试一下分类,创建一个Person的分类,并在分类里重写setName:方法,发现是可行的。但此时有一个隐患存在,因为此时我们已经在分类中实现了setName:方法,等于就是替换掉了Person类的setName:方法,此时Person类的setName:方法就不会被调用,而此时如果又需要重写Person类的setName:方法,那么就会出现影响。

KVO 底层实现:首先KVO需要创建一个子类(NSKVONotyfing_Person),这个子类是继承于被观察对象的,这个子类需要重写属性的setter方法,这个时候,外界在调用setter方法的时候,调用的是子类重写的setter方法。就是让外界的person对象的isa指针指向这个子类。

在添加观察者的地方打个断点来看一下:
isa指向.png

。此时Person类对象的isa指针指向的就是子类对象。

5. 自定义KVO

首先创建一个NSObject的分类:

//  NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)

-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
//  NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>

@implementation NSObject (KVO)


-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    //创建一个类
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName];
    Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
    //注册类
    objc_registerClassPair(MyClass);  //MyClass继承于self.class 根据案例来看,此时MyClass继承于Person,那么此时MyClass这个子类是否具有父类Person的setName:方法呢? 没有,只不过我们在调用方法的时候,子类继承于父类,如果子类没有实现方法,回去父类中调用该方法,所以在潜意识上,我们人为子类具有父类的方法,所谓的重写子类的方法,其实就是给这个子类添加一个方法。
   
    //重写setName方法
    class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
    //class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
    //参数名称   参数
    //Class  给那个类添加方法
    //SEL  方法编号
    //IMP  方法实现(指针)
    //types  返回值类型
    
    //修改isa指针
    object_setClass(self, MyClass);
    
    //将观察者保存到当前对象
    objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
    
}
void  setName(id self,SEL _cmd,NSString * newName){
    NSLog(@"%@",newName);
    
    //调用父类的setName:方法
    Class class =  [self class];//拿到当前类型
    object_setClass(self, class_getSuperclass(class));//修改当前类型,变成父类
    
    objc_msgSend(self, @selector(setName:),newName);
 
    //拿到Observer,
   id observer = objc_getAssociatedObject(self, @"observer");
    if (observer) {
        objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",@{@"name":newName,@"kind":@1},nil);
    }
    
    //改回子类
    object_setClass(self, class);
}
@end

这么写的KVO不会覆盖父类的set方法,也不会因为没有在dealloc中remove掉observer而崩溃掉。

6. 容器类的KVO
//  Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong)NSMutableArray * array;

@end


//  Person.m

-(NSMutableArray *)array
{
    if (!_array) {
        _array = [NSMutableArray array];
    }
 return _array;
}

//  ViewController.m
[_p addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];

其实注册观察者的步骤与属性时一样的,只不过在修改array的时候有些变化,因为KVO监听的是set方法,而对array进行操作却不是set方法,这时候其实KVO提供了一个方法:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray *tempArray = [_p mutableArrayValueForKey:@"array"];
    [tempArray addObject:@"xxxx"];
   //利用tempArray 去进行操作
}

通过断点来看一下tempArray的类型:


tempArray.png

最后补充几个注意:
1.kvo的本质是什么?
利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的子类,当修改instance对象的属性时,会调用willChangeValueForKey和didChangeValueForKey( 在父类原来的setter方法)并调用内部会触发监听器的监听方法(observerValueForKeyPath:)。

2.直接修改成员变量会触发KVO么?
不会触发KVO,添加KVO的Person实例,其实是NSKVONotyfing_Person类,再调用setter方法,不是调用Person的setter方法,而是NSKVONotyfing_Person的setter方法,因为修改成员变量不是setter方法赋值。

3.如果在项目中对Person类进行了监听,也创建了一个NSKVONotifying_Person类,那么会编译通过么?
编译通过,因为KVO是运行时刻创建的,并不在编译时刻,在编译时刻只有一个NSKVONotifying_Person,所以不报错,可以通过,但是此时KVO起不了作用。(KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class)

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

推荐阅读更多精彩内容

  • 上一篇:iOS-KVC浅谈 前言:KVO 作为 KVC 的同袍兄弟,功能更强大,聊聊 KVO。 一、KVO 简介 ...
    梦蕊dream阅读 711评论 0 0
  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 10,049评论 11 33
  • iOS--KVO的实现原理与具体应用 长时间不用容易忘,这篇文章挺好的.转载自看本文分为2个部分:概念与应用。概念...
    超_iOS阅读 1,436评论 0 17
  • 概述 KVO全称为Key Value Observing,键值监听机制,由NSKeyValueObserving协...
    rightmost阅读 1,015评论 0 0
  • 任何情绪的锈迹都在加速中 畅快地盛开 然后包围你 瘦小潮湿的身体 从襁褓到化石 无处不在的摇曳 开满任意一个季节 ...
    乌鸦之白阅读 135评论 0 0