iOS KVO实现原理和FBKVOController的使用

我们通常需要监听一个对象的某个属性值的变化,来动态的修改UI或者展示;

这时候KVO就排上了用场,KVO是苹果专门提供的用于监听某个对象的属性变化的方法;

例如:
要监听一个person对象的属性age值的变化,实现步骤如下;

1.系统KVO的使用

1、 给对象添加一个observer:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[YYPerson alloc]init];
    [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}

2、 实现observer回调方法:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"age = %d",[[change objectForKey:@"new"] intValue]);
    }
}

3、 在dealloc方法中移除观察者:

- (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

2.KVO实现原理

要弄清楚KVO的实现原理,我们就得知道,在添加观察者和没添加观察者之间有什么区别呢?

如下图,一个添加了observer的person1和没有添加observer的person2,打印其isa指针,区别如下:

添加监听后isa指针变化

person1和person2的区别:

  • person1的 isa指针 指向: NSKVONotifying_YYPerson

  • person2的 isa指针 指向: YYPerson

2.1 未使用KVO监听的对象:

  • 直接调用父类的setAge方法,改变成员变量_age的值;
未添加KVO对象

2.2 添加了KVO监听的对像:

会通过runtime动态的生成一个 NSKVONotifying_YYPerson 的中间类;

  • 然后实例对象person1的 isa指针 指向这个新生成的类;

  • NSKVONotifying_YYPerson 是YYPerson的子类,所以它的superclass指针指向YYPerson;

添加KVO对象

伪代码 模拟 NSKVONotifying_YYPerson 内部实现:

// .h

#import "YYPerson.h"

@interface NSKVONotifying_YYPerson : YYPerson

@end

//.m

@implementation NSKVONotifying_YYPerson

- (void)setAge:(int)age{
    _NSSetIntValueAndNotify();
}

//_NSSetIntValueAndNotify() 方法为Foundation框架中的方法,此处为伪代码,模拟实现
void _NSSetIntValueAndNotify(){

    [self willChangeValueForKey:@"age"];
    
    //真正的去改变父类中的age值
    [super setAge:age];
    
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
    //通知监听器, XXX属性值改变
    [observer observeValueForKeyPath:@"age" ofObject:self change:value context:nil];
}

@end

  • person1.age调用流程:

    • 通过person1的 isa指针 找到 NSKVONotifying_YYPerson中的setAge方法并调用

    • NSKVONotifying_YYPerson 中的setAge方法,会去调用Foundation框架中的方法_NSSetIntValueAndNotify();

    • _NSSetIntValueAndNotify()方法会依次调用如下方法:

      • 调用willChangeValueForKey();

      • [super setAge:age]; 调用YYPerson中的setAge方法,真正改变age属性的值;

      • didChangeValueForKey() 通知监听器,age的值变化了,回调观察者中实现的回调方法;

重写YYPerson中的方法willChangeValueForKey和didChangeValueForKey验证调用流程:

重写YYPerson中的方法

2.3 NSKVONotifying_YYPerson中其它被重写的方法

1、 通过[self.person1 class]和runtime获取类方法object__getClass(self.person1)对比,可以发现中间类NSKVONotifying_YYPerson 重写了class方法,因为苹果不希望开发者知道这个类的存在,所以重写这个方法

打印class

在中间类中,NSKVONotifying_YYPerson还重写了其他哪些方法,可以使用以下打印method列表方法打印一下:

- (void)printMethodList:(Class )cls{
    
    unsigned int count;
    //获得类的方法数组
    Method *methodList =  class_copyMethodList(cls, &count);
    
    //遍历所有的方法
    NSMutableString *methodString = [NSMutableString string];
    for (int i = 0; i < count; i ++) {
        Method method = methodList[i];
        NSString *mstring = NSStringFromSelector(method_getName(method));
        //拼接方法名
        [methodString appendString:mstring];
        [methodString appendString:@","];
    }
    free(methodList); //methodList是通过C语言copy得到的对象,需要释放
    NSLog(@"方法列表:%@",methodString);
}

分别传入person1和person2的类对象,打印结果:

打印方法列表

这3个方法简单实现如下,其中class方法直接返回的是[YYPerson class],这样可以不让外界知道这个类的存在:

- (Class)class{
    return [YYPerson class];
}

- (void)dealloc{
    //在移除观察者的时候做收尾工作
}

- (BOOL)isKVOA{
    return YES; //是不是KVO
}

2.4 验证

2、 验证 NSKVONotifying_YYPerson 的存在

验证中间类的存在

3、 验证 NSKVONotifying_YYPerson 中的setAge调用了Foundation中的_NSSetIntValueAndNotify()方法

_NSSetIntValueAndNotify()方法

4、 _NSSetIntValueAndNotify()方法和属性的类型是int,double,char是对应的:

image.png

如何手动触发KVO:

调用 willChangeValueForKey方法和didChangeValueForKey方法:

- (void)test{
    [self.person willChangeValueForKey:@"age"];
    [self.person didChangeValueForKey:@"age"];
}

按道理讲直接调用didChangeValueForKey就会触发监听的回调,但是didChangeValueForKey方法内部会做判断,是否调用过willChangeValueForKey,所以只调用didChange方法是不管用的,必须同时调用两个方法

3.FBKVOController 的使用

1、 实例化一个controller对象,并添加监听:

    //初始化后有一个是否强引用观察者参数retainObserved:NO可以避免循环引用问题
    self.fbVC = [[FBKVOController alloc]initWithObserver:self retainObserved:NO];

    [self.fbVC observe:self.person1 keyPath:@"age" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"age = %d",[[change objectForKey:@"new"] intValue]);
    }];

2、取消监听 (可以不取消)

注意: FBKVOController的一个特点就是:不移除observer,不会崩溃 所以这一步可以省略

- (void)dealloc{
    [sekf.fbVC unobserve:self.person1];
    //[self.fbVC unobserveAll];�  //或者可以取消全部的监听
    NSLog(@"%s",__func__);
}

3.1 系统KVO和FBKVOController优缺点

1.系统KVO的问题

  • 当观察者被销毁之前,需要手动移除观察者,否则会出现程序异常(向已经销毁的对象发送消息);
  • 可能会对同一个被监听的属性多次添加监听,这样我们会接收到多次监听的回调结果;
  • 当观察者对多个对象的不同属性进行监听,处理监听结果时,需要在监听回调的方法中,作出大量的判断;
  • 当对同一个被监听的属性进行两次removeObserver时,会导致程序crash。这种情况通常出现在父类中有一个KVO,在父类的dealloc中remove一次,而在子类中再次remove。

2、FBKVOController的优点

  • 可以同时对一个对象的多个属性进行监听,写法简洁;
  • 通知不会向已释放的观察者发送消息;
  • 增加了block和自定义操作对NSKeyValueObserving回调的处理支持;
  • 不需要在dealloc 方法中手动移除观察者,而且移除观察者不会抛出异常,当FBKVOController对象被释放时, 观察者被隐式移除;

例如:如下面代码,添加一个监听属性的方法有单独的block实现,不需要和系统的KVO那样在同一个方法里面去判断,解耦,看起来也简洁

    [self.fbVC observe:self.person1 keyPath:@"age" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"age = %d",[[change objectForKey:@"new"] intValue]);
    }];
    
    [self.fbVC observe:self.person2 keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
        NSLog(@"age = %@",[[change objectForKey:@"new"] intValue]);
    }];

关于FBKVOController实现原理,可参考文章链接

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