KVO底层实现原理,仿写KVO
这篇文章简单介绍苹果的KVO底层是怎么实现的,自己仿照KVO的底层实现,写一个自己的KVO监听
#pragma mark--KVO底层实现
第一步:新建一个Person类继承NSObject
Person.h
#import <Foundation/Foundation.h> @interface Person : NSObject //字符串类型的属性name
@property (nonatomic, strong) NSString *name; @end
Person.m
#import "Person.h" @implementation Person
- (void)setName:(NSString *)name
{ //别问为什么(下面有用处),就是要自己处理set方法
_name = [NSString stringWithFormat:@"%@aaaa",name];
}
@end
第二步:在控制器中创建一个Person类型的对象p,利用苹果的KVO来监听该对象p的name属性的变化
ViewController.h
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @end
ViewController.m
#import "ViewController.h"
#import "Person.h" @interface ViewController () @property (nonatomic, strong) Person *p; @end @implementation ViewController - (void)viewDidLoad
{
[super viewDidLoad]; Person *p = [[Person alloc] init]; // 监听name属性有没有改变
[p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.p = p;
} //点击屏幕修改self.p的name属性的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
static int i = ;
i++;
self.p.name = [NSString stringWithFormat:@"%d",i];
} // 所有的KVO监听都会来到这个方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
NSLog(@"%@",self.p.name);
} @end
会打印1aaaa,2aaaa,3aaaa...... //从打印可以看出KVO监听的是set方法的调用!!!
实际上:KVO的本质就是监听一个对象有没有调用set方法!!!
怎么验证呢?
1.将Person.h中的代码修改为
#import <Foundation/Foundation.h> @interface Person : NSObject
{
@public
NSString *_name;//为了验证KVO监听的是setter方法
} @end
将ViewController.m的viewDidLoad中的代码修改为
- (void)viewDidLoad
{
[super viewDidLoad]; Person *p = [[Person alloc] init]; // 监听_name有没有改变
[p addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew context:nil];
self.p = p;
}
运行后,点击屏幕是没有任何打印的-->结论1:KVO的本质就是监听一个对象有没有调用set方法!!!
第三步:打断点看一下addObserver 到底干了什么事情?

继续走一步:

这个NSKVONotifying_Person类是什么鬼?
估计你已经猜到了! 没错这个NSKVONotifying_Person类就是系统帮我们实现的!!!
怎么验证呢? 想到了吗?我们自己把这个类给重写了!看看会发生什么?
第四步:创建NSKVONotifying_Person类
NSKVONotifying_Person.h
#import <Foundation/Foundation.h> @interface NSKVONotifying_Person : NSObject @end
NSKVONotifying_Person.m
#import "NSKVONotifying_Person.h" @implementation NSKVONotifying_Person @end
第五步运行:

恭喜猜测没错,验证通过!!!
结论2:系统创建了一个NSKVONotifying_XXX的类
结论3:修改了对象p的isa指针
那么系统帮我们创建的NSKVONotifying_XXX类有没有继承自我们自己常见的XXX类呢?
我既然这么问,肯定是继承啦!!因为我们已经把属性对应的set方法给重写了!!!因为已经修改了对象的指针(用户调用对象p的方法就不是Person的方法了,是isa指针指向的类的对应的方法),如果不继承的话,相当于把用户重写的set方法给覆盖了,用户调用自己写的set方法就不起作用了!!!也就有了 结论4:重写对应的set方法再内部实现父类做法,通知观察者
KVO底层实现结论:(都是系统自动帮我们实现的)
1> 创建NSKVONotifying_XXX的类
2> 重写对应属性的set方法,在内部实现父类做法,通知观察者
3> 修改当前对象的isa指针,指向创建的NSKVONotifying_XXX(这样实际调用的时候就会走NSKVONotifying_XXX类中对应的方法)
#pragma mark--仿写KVO实现
知道了KVO底层实现的原理就可以仿照KVO写一个自己的"KVO"了
步骤一:给NSObject写一个分类
NSObject+KVO.h
#import <Foundation/Foundation.h> @interface NSObject (KVO) //添加监听
- (void)ey_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; //移除监听
- (void)ey_removeObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath context:(nullable void *)context; @end
NSObject+KVO.m
//
// NSObject+KVO.m
// KVO底层实现
//
// Created by lieryang on 2017/6/17.
// Copyright © 2017年 lieryang. All rights reserved.
// #import "NSObject+KVO.h"
#import <objc/message.h> static NSString * const EYKVONotifying_ = @"EYKVONotifying_";
static NSString * const observerKey = @"observer";
@implementation NSObject (KVO) //添加监听
- (void)ey_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
if (keyPath.length == ) {//如果传进来的keyPath为@""或者为nil 直接返回
return;
} // 1. 检查对象的类有没有相应的 setter 方法。
SEL setterSelector = NSSelectorFromString([self setterForGetter:keyPath]); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) {//如果没有直接返回,不需要任何处理
NSLog(@"找不到该方法");
return;
} // 2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类
Class clazz = object_getClass(self);
NSString *className = NSStringFromClass(clazz); if (![className hasPrefix:EYKVONotifying_]) {
clazz = [self ey_KVOClassWithOriginalClassName:className];
object_setClass(self, clazz);
} // 3. 为EYKVONotifying_XXX添加setter方法的实现
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)ey_setter, types); // 4. 添加该观察者到观察者列表中
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey));
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
if ([observers indexOfObject:observer] == NSNotFound) {
[observers addObject:observer];
}
} //移除监听
- (void)ey_removeObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nullable)keyPath context:(nullable void *)context
{ NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(observerKey)); [observers removeObject:observer];
}
#pragma mark - 注册自己的EYKVONotifying_XXX
- (Class)ey_KVOClassWithOriginalClassName:(NSString *)className
{
// 生成EYKVONotifying_XXX的类名
NSString *kvoClassName = [EYKVONotifying_ stringByAppendingString:className];
Class kvoClass = NSClassFromString(kvoClassName); // 如果EYKVONotifying_XXX已经被注册过了, 则直接返回
if (kvoClass) {
return kvoClass;
} // 如果EYKVONotifying_XXX不存在, 则创建这个类
Class originClass = object_getClass(self);
kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, ); // 修改EYKVONotifying_XXX方法的实现, 学习Apple的做法, 隐瞒这个EYKVONotifying_XXX
Method classMethod = class_getInstanceMethod(kvoClass, @selector(class));
const char *types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)ey_class, types); // 注册EYKVONotifying_XXX
objc_registerClassPair(kvoClass); return kvoClass;
} Class ey_class(id self, SEL cmd)
{
Class clazz = object_getClass(self); // EYKVONotifying_XXX
Class superClazz = class_getSuperclass(clazz); // origin_class
return superClazz; // origin_class
} /**
* 重写setter方法, 新方法在调用原方法后, 通知每个观察者
*/
static void ey_setter(id self, SEL _cmd, id newValue)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = [self getterForSetter:setterName]; if (!getterName) {
NSLog(@"找不到getter方法");
} // 调用原类的setter方法
struct objc_super superClazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// 这里需要做个类型强转, 否则会报too many argument的错误
((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue); // 找出观察者的数组
NSArray *observers = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(observerKey));
// 遍历数组
for (id observer in observers) {
// 调用监听者observer的observeValueForKeyPath 方法,因为observer为id类型,所以就偷懒调用了系统的监听回调,要是自己定义方法,会报找方法的错误,可以在添加监听的时候,传进来一个block代码块,在此处回调block,更方便外界的调用
[observer observeValueForKeyPath:getterName ofObject:self change:nil context:nil];
}
} #pragma mark - 生成对应的setter方法字符串
- (NSString *)setterForGetter:(NSString *)key
{
// 1. 首字母转换成大写
NSString * firstString = [[key substringToIndex:] uppercaseString];
// 2. 剩下的字母
NSString * remainingString = [key substringFromIndex:]; // 3. 最前增加set, 最后增加: setName:
NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstString, remainingString]; return setter; }
#pragma mark - 生成对应的getter方法字符串
- (NSString *)getterForSetter:(NSString *)key
{
// setName
if (key.length <= || ![key hasPrefix:@"set"] || ![key hasSuffix:@":"]) {
return nil;
} // 移除set和:
NSRange range = NSMakeRange(, key.length - );
NSString *getter = [key substringWithRange:range]; // 小写
NSString *firstString = [[getter substringToIndex:] lowercaseString];
getter = [getter stringByReplacingCharactersInRange:NSMakeRange(, )
withString:firstString]; return getter;
}
@end
外界使用
创建Person类
Person.h
#import <Foundation/Foundation.h> @interface Person : NSObject @property (nonatomic, copy) NSString * name; @end
Person.m
#import "Person.h" @implementation Person @end
#import "ViewController.h"
#import "Person.h"
#import "NSObject+KVO.h" @interface ViewController () @property (nonatomic, strong) Person * p; @end @implementation ViewController - (void)viewDidLoad {
[super viewDidLoad]; Person * p = [[Person alloc] init];
[p ey_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.p = p;
} // 监听的回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
NSLog(@"%@", self.p.name);
} - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event]; self.p.name = [NSString stringWithFormat:@"%u", arc4random_uniform()];
} @end
更多内容--> 博客导航 每周一篇哟!!!
有任何关于iOS开发的问题!欢迎下方留言!!!或者邮件lieryangios@126.com 虽然我不一定能够解答出来,但是我会请教iOS开发高手!!!解答您的问题!!!
KVO底层实现原理,仿写KVO的更多相关文章
- Object-C知识点 (三) 单例 蒙版 刷新 KVO底层
#pragma mark - 单例方法(完整的方法) 系统的单例方法名称 sharedApplication defaultManager standardUserDefaults currentDe ...
- KVC和KVO的理解(底层实现原理)
1.KVC,即是指 NSKeyValueCoding,一个非正式的Protocol,提供一种机制来间接访问对象的属性.而不是通过调用Setter.Getter方法访问.KVO 就是基于 KVC 实现的 ...
- IOS-详解KVO底层实现
一.KVO (Key-Value Observing) KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现.也是 Cocoa Binding 的基础.当被观察对 ...
- KVO内部实现原理
KVO的原理: 只要给一个对象注册一个监听, 那么在运行时, 系统就会自动给该对象生成一个子类对象, (格式如:NSKVONotifying_className), 并且重写自动生成的子类对象的被监听 ...
- PHP底层工作原理
最近搭建服务器,突然感觉lamp之间到底是怎么工作的,或者是怎么联系起来?平时只是写程序,重来没有思考过他们之间的工作原理: PHP底层工作原理 图1 php结构 从图上可以看出,php从下到上是一个 ...
- Java并发之底层实现原理学习笔记
本篇博文将介绍java并发底层的实现原理,我们知道java实现的并发操作最后肯定是由我们的CPU完成的,中间经历了将java源码编译成.class文件,然后进行加载,然后虚拟机执行引擎进行执行,解释为 ...
- Spring(二)IOC底层实现原理
IOC原理 将对象创建交给Spring去管理. 实现IOC的两种方式 IOC配置文件的方式 IOC注解的方式 IOC底层实现原理 底层实现使用的技术 1.1 xml配置文件 1.2 dom4j解析xm ...
- iOS分类底层实现原理小记
摘要:iOS分类底层是怎么实现的?本文将分如下四个模块进行探究分类的结构体编译时的分类分类的加载总结本文使用的runtime源码版本是objc4-680文中类与分类代码如下//类@interfaceP ...
- java并发编程系列七:volatile和sinchronized底层实现原理
一.线程安全 1. 怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...
随机推荐
- 关于Dubbo一个接口多个实现的解决方案
如题,其实这个问题在官方文档中已经说明了.我直接贴图就好了 更多学习请参考:minglisoft.cn/technology
- Android码农如何一个星期转为iOS码农(不忽悠)
WeTest 导读 作为一个android客户端开发,如果你不懂点ios开发,怎么好意思说自己是客户端开发呢,本文讲解如何让android开发码农在一个星期上手IOS开发 --<记录自己IOS开 ...
- 当一个JavaScripter初次进入PHP的世界,他将看到这样的风景
本文将从以下11点介绍javascript和PHP在基础语法和基本操作上的异同: 1.数据类型的异同 2.常量和变量的定义的不同,字符串连接运算符不同 3.对象的创建方法的不同 4.PHP与JS在变 ...
- VR全景加盟-了解VR就来全景智慧城市
关于什么是真正的VR说了这么多,面对刚刚起步的VR,如何辨别判断一个真正的VR形式呢.除了我们所说几个参数或者大家关注的眩晕感.临场感,真正的VR究竟带给大家什么样的特性呢?这个就要从VR的本质谈起. ...
- 使用FileUtils简化你的文件操作
前言: 在工作当中我们往往遇到很多文件的操作,我们也习惯写一些自己定义的工具类来简化文件操作,其实apache的commons的FileUtils类就是这样一个工具类,使用它能大大的简化我们对文件的操 ...
- javaWeb学习总结(11)- 监听器(Listener)学习(2)
一.监听域对象中属性的变更的监听器 域对象中属性的变更的事件监听器就是用来监听 ServletContext, HttpSession, HttpServletRequest 这三个对象中的属性变更信 ...
- (数字IC)低功耗设计入门(四)——RTL级低功耗设计
二.RTL级低功耗设计 前面介绍了系统级的低功耗设计,换句话说就是在系统级降低功耗可以考虑的方面.系统级的低功耗设计,主要是由系统级设计.具有丰富经验的人员实现,虽然还轮不到我们设计,我们了解一下还是 ...
- eclipse hibernate导出数据库实体类
打开eclipse->help->Eclipse Marketplace->查找hibernate->安装如下插件 只要安装其中一个,hibernate tool即可: 安装完 ...
- Vue2.0 全家桶开发的网页应用(参照吾记APP)
github链接 借鉴吾记APP,使用 vue2.0+vue-router+vuex 为主要技术栈,elementui做为ui框架,多模块 spa 模式,webpack2.0 负责模块打包,gulp ...
- linux定时任务访问url
这次linux定时任务设置成功,也算是自己学习linux中一个小小的里程碑.:) 撒花撒花--- 以下操作均是在ubuntu 下操作的,亲测有效,其他的linux系统还望亲们自己去查.鞠躬感谢! 1 ...