iOS响应式编程—KVO

一、KVO的概念

KVO(key-value-observing)是 Objective-C 对观察者设计模式的一种实现,是一种十分有趣的回调机制,在某个对象注册监听者后,在被监听的对象发生改变时,对象会发送一个通知给监听者,监听者作出相应的处理。

二、KVO分析

先来看看苹果文档对于KVO的实现描述

Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ..

KVO 的实现依赖于 Objective-C 强大的 Runtime,文档中关于KVO的实现没有作详细的描述,但是我们知道了KVO是利用Runtime实现后,我就在想我们自己能不能模拟实现一个KVO的实现,下面我们来分析下KVO的具体实现:

isa指针:

isa(isa-swizzling)这个指针,isa是一个Class类型的指针,对象的首地址一般是isa变量,同时isa又保存了对象的类对象的首地址。我们通过object_getClass方法来获取这个对象的元类,即是对象的类对象的类型。

isa作用:

isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。

下面我们来看下原生的KVO对象被观察前后的isa指针的变化:

Person *_p = [[Person alloc] init];
NSLog(@"被观察前use runtime to get class: %@", object_getClass(_p));

[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"被观察后use runtime to get class: %@", object_getClass(_p));
2018-03-29 11:01:49.754404+0800 iOS—响应式编程[1811:53355] 被观察前use runtime to get class: Person
2018-03-29 11:02:05.413029+0800 iOS—响应式编程[1811:53355] 被观察后use runtime to get class: NSKVONotifying_Person

从上面代码执行中我们可以看出,当_p对象被观察后,_P对象的isa指针指向了NSKVONotifying_Person,这个时候我们就要思考一个问题了,_p对象的指针是什么时候被改变了呢?这时候我们就要明白KVO内部实现原理了。

KVO原理:

Apple 官方使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO使用Runtime创建一个名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,然后为 NSKVONotifying_A 重写观察属性的 setter 方法,并且在A对象被观察后把A的isa指针指向NSKVONotifying_A,调用setter 方法,通知观察对象属性值的更改情况。
总结一句话:动态创建一个子类,添加重写setter方法。

内部具体实现步骤(结合下面代码,代码中第x步对应这里的步骤):

  1. 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类NSKVONotifying_A 。
  2. 为新建的子类NSKVONotifying_A添加一个setter方法,重写了父类被观察属性 keyPath 的 setter 方法。
  3. 改变被观察对象的 isa 指针,从指向原来的 A 类改为指向新创建的子类 NSKVONotifying_A 类。(isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象,因而在该对象上对 setter 的调用就会调用已重写的 setter)
  4. 实现子类的setter方法
  5. 使用Runtime的关联机制,在添加观察对象的时候关联观察者,在setter方法中取出被观察者。
  6. objc_msgSend Runtime消息发送通知观察的改变
// 新建一个person类
import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

// 新建一个类别:NSObject+KVO
- (void)Jey_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    // 第一步
    NSString *oldClass = NSStringFromClass([self class]);
    NSString *newClass = [NSString stringWithFormat:@"JEYKVO_%@",oldClass];
    const char *className = [newClass UTF8String];
    // 定义一个类
    Class myclass = objc_allocateClassPair([self class], className, 0);

    // 第二步
    // 为这个类添加一个set方法,子类重写
    class_addMethod(myclass, @selector(setName:), (IMP)setName, "");
    // 注册这个类
    objc_registerClassPair(myclass);
    
    // 第三步
    // 改变isa指针指向,isa指针指向子类
    object_setClass(self, myclass);
    
    // 第五步
    // 下一步是怎么通知observer的过程
    // 关联对象
    objc_setAssociatedObject(self, (__bridge const void*)@"objc", observer, OBJC_ASSOCIATION_RETAIN);
}

// 第四步
void setName (id self, SEL _cmd, NSString *newName) {
    
    id classs = [self class];
    object_setClass(self, class_getSuperclass([self class]));
    objc_msgSend(self, @selector(setName:),newName);
    
    // 取出关联的对象
    id observer = objc_getAssociatedObject(self, (__bridge const void*)@"objc");
 
    // 第六步
    NSDictionary *dict =@{@"newName":newName};
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, dict, nil);
    
    // 改回类型
    object_setClass(self, classs);
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
//    _p = [[Person alloc] init];
//    NSLog(@"被观察前use runtime to get class: %@", object_getClass(_p));
//
//    [_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
//    NSLog(@"被观察后use runtime to get class: %@", object_getClass(_p));

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,709评论 0 9
  • 面试题参考1 : 面试题[http://www.cocoachina.com/ios/20150803/12872...
    江河_ios阅读 1,731评论 0 4
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,017评论 0 26
  • 一、概述 KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则其观察...
    DeerRun阅读 10,055评论 11 33
  • 本文分为2个部分:概念与应用。概念部分旨在剖析 KVO 这一设计模式的实现原理;应用部分通过创建的项目,以说明 K...
    啊左阅读 57,699评论 107 438