KVO使用和原理

KVO是OC中观察者模式的一种实现,一个对象监测另一对象某属性是否发生变化,当被观察者某个属性发生改变时,会触发观察者的 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context 这个回调方法被执行

1、触发模式

首先添加观察者

  [self.p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];

KVO在属性发生变化时是自动调用的,如果想手动调用或自己实现KVO属性的调用,则可通过在被观察者中实现automaticallyNotifiesObserversForKey,设置返回NO

//关闭自动观察者监听回调
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

//在修改属性的地方调用willChangeValueForKey和didChangeValueForKey

[self.p willChangeValueForKey:@"name"];
self.p.name = [NSString stringWithFormat:@"%@+",self.p.name];
[self.p didChangeValueForKey:@"name"];

则可以在观察者的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context中监听到变化

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"change=%@",change);
}

最后记得当观察者不需要监听时或者观察者在销毁前记得移除观察者

 [self.p removeObserver:self forKeyPath:@"name"];

由上述可见:不自动调用观察回调的情况下,在存取数值的前后分别调用 2 个方法,也可以在监听回调中收到新值,\color{red}{说明KVO 的键值观察通知依赖于 NSObject 的willChangeValueForKey:和 didChangevlueForKey:}
而在自动调用观察回调的情况下,只需要通过 .(点)语法修改,也可以在监听回调中监听,\color{red}{说明KVO 的键值观察通知和setter方法有关}

2、KVO实现原理

我们在添加观察者前后打印出p的类


添加观察者前.png

添加观察者后.png

当被观察者(例如Person)为属性(name)添加观察者时,KVO在运行时动态创建了一个子类(NSKVONotifying_Person),并在这个子类中重写了(name)的setter方法,在其中观察属性变化情况并通知观察者的处理,而setter内部调用willChangeValueForKey和didChangeValueForKey方法并触发了observeValueForKey:ofObject:change:context: 回调方法
实现步骤:
1)创建了子类
2)重写了setter方法
3)改变了isa的指针

3、自定义KVO

我以Person为例(先写部分代码,后面会给全)
1、创建分类,分类中自定义添加观察者方法
2、在实现中创建、注册子类
objc_allocateClassPair、objc_registerClassPair
3、重写setter方法(因为子类中没有父类的方法,所以重写实质是添加了一个方法),例如 class_addMethod(kidClass, selector, (IMP)setterMethodCustom, "v@:@");
class_addMethod
4、修改isa指针
object_setClass
5、因为属性修改要告诉观察者,所以需要关联观察者属性绑定
objc_setAssociatedObject
6、在内部实现修改属性的方法void setterMethodCustom (id self,SEL cmd,id value)中,因为需要先调用父类的setter方法,所以需要
1)先保存当前类 Class kidClass = [self class];
2)修改isa指向父类object_setClass(self, class_getSuperclass(kidClass));
3)调用父类的setter方法((void ()(id,SEL,id)) objc_msgSend)(self, cmd, value);
在获取观察者objc_getAssociatedObject
通知观察者执行观察方法((void (
)(id, SEL,id,id,id,id))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,@{@"old":oldValue,@"new":value},nil);
代码如下:

 #import "Person.h"
@interface Person (KVOExt)
- (void)ymj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end
#import "Person+KVOExt.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Person (KVOExt)
- (void)ymj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    //自定义子类的类名
    NSString * kidClassName = [NSString stringWithFormat:@"YMJKVONOtifying_%@",NSStringFromClass([self class])];
    //创建子类
    Class kidClass = objc_allocateClassPair(self.class, [kidClassName UTF8String], 0);
    //注册子类
    objc_registerClassPair(kidClass);
    
    //setter方法名
    NSString *firstString = [keyPath substringToIndex:1];
    firstString = [firstString uppercaseString];
    NSString *setterString = [keyPath substringFromIndex:1];
    NSString * setterSelName = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
    //重写setter方法,因为子类中没有父类的方法,所以重写实质是添加了一个方法
    SEL selector = NSSelectorFromString(setterSelName);
    class_addMethod(kidClass, selector, (IMP)setterMethodCustom, "v@:@");
    //v@:@ => v表示返回值void,@表示对象,:表示SEl,因为oc方法中默认有两个参数(和消息发送机制有关),一个self,一个cmd,所以这里代表返回值为void,第一个参数self,第二个参数cmd,第三个参数要设置的新值value
 
    //修改isa指针
    object_setClass(self, kidClass);
    
    //因为属性修改时候要告诉观察者,所以给子类添加属性关联
    objc_setAssociatedObject(self, @"ymj_observer", observer, OBJC_ASSOCIATION_ASSIGN);

    
}



void setterMethodCustom (id self,SEL cmd,id value){
    
    NSLog(@"value=%@",value);
    //根据setter方法名获取属性key
    NSString * setterSelName = NSStringFromSelector(cmd);
    setterSelName = [setterSelName stringByReplacingOccurrencesOfString:@"set" withString:@""];
    setterSelName = [setterSelName stringByReplacingOccurrencesOfString:@":" withString:@""];
    NSString * keyPath = [NSString stringWithFormat:@"%@%@",[[setterSelName substringToIndex:1] lowercaseString],[setterSelName substringFromIndex:1]];
    //获取老值
    id oldValue = [self valueForKey:keyPath];
    
    //更改isa指向父类,先执行父类的setter方法,保存属性的值
    Class kidClass = [self class];
    object_setClass(self, class_getSuperclass(kidClass));
    ((void (*)(id,SEL,id)) objc_msgSend)(self, cmd, value);
    
    //获取观察者对象
    id observer = objc_getAssociatedObject(self, @"ymj_observer");
    
    if(observer){
      //不能直接使用objc_msgSend的原型方法来匿名调用,否则会出现异常
      ((void (*)(id, SEL,id,id,id,id))objc_msgSend)(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,self,@{@"old":oldValue,@"new":value},nil);
    }
   //更改isa指向为子类
    object_setClass(self, kidClass);
}

4、KVO属性依赖

观察被观察者(例如:Person类)的属性(例如:Dog类)的属性(例如:age)时候,有两种方法处理
(1)监听的keypath直接写被观察者(p)的对象属性(dog).属性(age),例如"dog.age"

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

(2)如果不仅仅监听age属性,还有dog中其他属性,推荐使用第二种方法,监听的keypath还是写被观察者(p)的属性(dog),而在被观察者(Person类)的内部实现keyPathsForValuesAffectingValueForKey方法,返回要观察的对象的属性的响应的属性,例如"_dog.age","_dog.level",则在修改dog的属性时,也能触发监听回调

//添加观察者
[self.p addObserver:self forKeyPath:@"dog"  options:(NSKeyValueObservingOptionNew) context:nil];
//在Person内部
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    if([key isEqualToString:@"dog"]){
        return [NSSet setWithObjects:@"_dog.age",@"_dog.level", nil];
    }
    return [super keyPathsForValuesAffectingValueForKey:key];
}

5、KVO对容器类型的监听

因为KVO是通过在子类setter方法中触发监听回调,而单纯的使用addobject不会调用子类setter方法触发监听回调,测试发现有两种方法
(1)在修改前通过[对象 mutableArrayValueForKey:]会返回容器类的子类(NSKeyValueMutableArray,如下图),再使用addobject就能触发观察监听了

1111111.png

 NSMutableArray * arr = [self.p mutableArrayValueForKey:@"dataArr"];
    [arr addObject:self.p.name];

(2)考虑到setter内部是和willChangeValueForKey和didChangeValueForKey有关,于是测试手动在addobject前后调用willChangeValueForKey和didChangeValueForKey也能触发回调监听

    [self.p willChangeValueForKey:@"dataArr"];
    [self.p.dataArr addObject:self.p.name];
    [self.p didChangeValueForKey:@"dataArr"];

不过这两种方法,在监听回调中,change信息会有所不同

Demo地址https://github.com/Ywuxing/KVODemo.git

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

推荐阅读更多精彩内容

  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,011评论 0 26
  • 该文章属于刘小壮原创,转载请注明:刘小壮[https://www.jianshu.com/u/2de707c93d...
    刘小壮阅读 48,236评论 35 227
  • KVC 什么是 KVC KVC 是 Key-Value-Coding 的简称。 KVC 是一种可以直接通过字符串的...
    LeeJay阅读 2,206评论 6 41
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • KVO使用 KVO(key-value-observing)键值监听常用来监听特定对象中某属性值的变化,日常开发中...
    Gintok阅读 760评论 0 1