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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

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

友情链接更多精彩内容