KVO实现原理

一.关于KVO

KVO(Key-Value Observing)提供一种机制,当指定对象的属性被修改后,就会通知观察者。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

KVO其实也是“观察者”设计模式的一种应用。这种模式有利于两个类间的解耦合,尤其是对于业务逻辑与视图控制这两个功能的解耦合。

二.KVO用法

假设已经有一个Person类,包含name属性,我们可以通过KVO监听到name属性被修改的事件:

_p = [Person new];
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

即当_p对象的name属性被修改后,会调用self的以下方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{NSLog(@"kvo监听到%@对象的%@属性被修改为%@",object,keyPath,[change objectForKey:@"new"]);
}

三.KVO实现原理

当调用以下函数前:

[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

我们可以看到_p的isa指针指向类Person:

但是当单步执行过后,_p的isa指针会变为指向NSKVONotifying_Person类

即对于上面的代码,系统会执行如下操作:

1、利用runtime动态创建Person类的子类NSKVONotifying_Person,重写name属性的set方法:

-(void)setName:(NSString*)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}

2、将p对象的isa指针指向新建的子类

当我们修改_p的name属性时,会执行NSKVONotifying_Person类对象的set方法。

四.自己实现KVO

我们可以按照上面的思路,自己用代码实现KVO的基本功能,同时优化为以block的方式进行回调而非使用-observeValueForKeyPath:ofObject:change:context:方法。

1.新建一个NSObjct的Category,声明添加监听和移除监听方法:
NSObject+KVO.h

typedef void(^JXObserveingBlock)(id observer,NSString *key,id oldValue,id newValue);@interface NSObject (KVO)-(void)JX_addObserver:(NSObject *)observer forKeyPath:(NSString *)key block:(JXObserveingBlock*)block;-(void)JX_removeObserver:(NSObject *)observer forKey:(NSString *)key;@end

2.实现JX_addObserver:forKeyPath:block:方法:

-(void)JX_addObserver:(NSObject *)observer forKeyPath:(NSString *)key block:(JXObserveingBlock)block{//1.检查对象是否存在对应的set方法,没有则抛出异常。SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);Method setterMethod = class_getInstanceMethod([self class], setterSelector);if (!setterMethod) {// throw invalid argument exception}//2.检查isa指针指向的类是否已经是KVO的类,不是则新建原类的子类,并把isa指针指向新类。Class class = object_getClass(self);NSString *className = NSStringFromClass(class);if (![className hasPrefix:kJXKVOClassPrefix]) {class = [self makeKvoClassWithOriginalClassName:className];object_setClass(self, class);}//3.检查KVO类是否已经重写了set方法,没有则重写set方法。if (![self hasSelector:setterSelector]) {const char *types = method_getTypeEncoding(setterMethod);class_addMethod(class, setterSelector, (IMP)kvo_setter, types);}//添加观察者JXObservationInfo *info = [[JXObservationInfo alloc] initWithObserver:observer Key:key block:block];NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void*)kJXKVOAssociatedObservers);if (!observers) {observers = [NSMutableArray array];objc_setAssociatedObject(self, (__bridge const void*)kJXKVOAssociatedObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}[observers addObject:info];
}

其中setterForGetter:方法是获取属性对应的set方法名:

-(NSString*)setterForGetter:(NSString*)key{if (key.length <= 0) {return nil;}//1.首字母大写key = [key capitalizedStringWithLocale:[NSLocale currentLocale]];//2.添加set前缀和:后缀NSString *setter = [NSString stringWithFormat:@"set%@:",key];return setter;
}

hasSelector:方法判断是否已经重写了set方法

- (BOOL)hasSelector:(SEL)selector{Class clazz = object_getClass(self);unsigned int methodCount = 0;Method* methodList = class_copyMethodList(clazz, &methodCount);for (unsigned int i = 0; i < methodCount; i++) {SEL thisSelector = method_getName(methodList[i]);if (thisSelector == selector) {free(methodList);return YES;}}free(methodList);return NO;
}

makeKvoClassWithOriginalClassName:方法根据类名返回kvo类:

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName{NSString *kvoClassName = [kJXKVOClassPrefix stringByAppendingString:originalClazzName];Class clazz = NSClassFromString(kvoClassName);if (clazz) {return clazz;}//如果还没创建对应的kvo类,则创建Class originalClazz = object_getClass(self);Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClassName.UTF8String, 0);//重写class方法Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));const char *types = method_getTypeEncoding(clazzMethod);class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);objc_registerClassPair(kvoClazz);return kvoClazz;
}static Class kvo_class(id self, SEL _cmd)
{return class_getSuperclass(object_getClass(self));
}

最后,重写setter 方法:

static void kvo_setter(id self, SEL _cmd, id newValue)
{NSString *setterName = NSStringFromSelector(_cmd);NSString *getterName = getterForSetter(setterName);if (!getterName) {NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];@throw [NSException exceptionWithName:NSInvalidArgumentExceptionreason:reasonuserInfo:nil];return;}id oldValue = [self valueForKey:getterName];struct objc_super superclazz = {.receiver = self,.super_class = class_getSuperclass(object_getClass(self))};// cast our pointer so the compiler won't complainvoid (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;// call super's setter, which is original class's setter methodobjc_msgSendSuperCasted(&superclazz, _cmd, newValue);// look up observers and call the blocksNSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kJXKVOAssociatedObservers));for (JXObservationInfo *each in observers) {if ([each.key isEqualToString:getterName]) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{each.block(self, getterName, oldValue, newValue);});}}
}static NSString * getterForSetter(NSString *setter)
{if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {return nil;}// 移除‘set’前缀以及后面的‘:’NSRange range = NSMakeRange(3, setter.length - 4);NSString *key = [setter substringWithRange:range];// 首字母小写NSString *firstLetter = [[key substringToIndex:1] lowercaseString];key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)withString:firstLetter];return key;
}

五.测试实现的KVO

把原来的:

[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

修改为:

[_p JX_addObserver:self forKeyPath:@"name" block:^(id observer, NSString *key, id oldValue, id newValue) {NSLog(@"kvo监听到%@对象的%@属性被修改为%@",observer,key,newValue);}];

在修改_p的name属性之后,会打印出修改信息。

完整代码下载:http://download.csdn.net/detail/dolacmeng/9849000

参考:http://tech.glowing.com/cn/implement-kvo/

利用runtime实现KVO相关推荐

  1. 利用Runtime修改UIdatePicker的字体颜色

    用过苹果原生UIdatePicker的Ios开发者都知道,UIdatePicker这个时间选择器的字体颜色默认的是黑色,并且官方API并没有提供可以修改字体颜色的API.如下: UIDatePicke ...

  2. 利用Runtime类,来操作电脑关机。。

    import java.io.IOException; public class Demo01 { public static void main(String[] args) throws Exce ...

  3. iOS 利用RunTime检测控制器是否销毁

    本文是在控制器的基础上添加了一个 Category ,只需导入 Category 并引用头文件,不需要在项目里引用其他代码,就可以检测控制器是否被销毁.具体代码如下: UIViewController ...

  4. 8 iOS中KVO 的本质

    前言本质 Automatic key-value observing is implemented using a technique called isa-swizzling 这计划的意思就是 自动 ...

  5. Runtime知识点整理

    Runtime介绍 Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象.进行消息传递和转发. Runtime就是这个运行时系统. Runti ...

  6. 浅谈runtime运行时机制

    由于OC是运行时语言,只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法.利用runtime机制让我们可以在程序运行时动态修改类.对象中的所有属性.方法. 下面就介绍运行时一种很简单的使 ...

  7. KVO 从基本使用到原理剖析

    文章目录 1. 简介 2. 基本使用 2.1 设置观察者 2.2 接收属性改变消息 2.3 移除观察者 2.4 KVO 使用实例 3. 原理剖析 3.1 KVO 的实现 3.2 NSKVONotify ...

  8. runtime实现的机制是什么,怎么用,一般用于干嘛. 你还能记得你所使用的相关的头文件或者某些方法的名称吗?...

    运行时机制,runtime库里面包含了跟类.成员变量.方法相关的API,比如获取类里面的所有成员变量,为类动态添加成员变量,动态改变类的方法实现,为类动态添加新的方法等 需要导入<objc/me ...

  9. iOS的runtime运行时机制

    本文转自http://www.cnblogs.com/guoxiao/p/3583432.html 最近一直在研究runtime运行时机制的问题,我想可能也有很多人不太清楚这个问题吧?在这里跟大家沟通 ...

最新文章

  1. IE和火狐都支持的方法(输入用户名和密码后按下 enter 键)
  2. MySQL基础:数据类型
  3. 前端开发代码架构相关想法
  4. scrapy从入门到放弃 学习项目2
  5. django-allauth定制模板(转载)
  6. spring mvc学习(45):springMVC的三大组件
  7. java 前台商品展示模块_SSH框架网上商城项目第10战之搭建商品类基本模块
  8. #pragma comment使用
  9. 如何在delphi里面控制Edit只能输入数字
  10. 技术总监和CTO的区别 浅谈CTO的作用----软件公司如何开源节流
  11. adventureworks mysql_AdventureWorks相关
  12. Error: The method ‘DioHttpHeaders.add‘ has fewer named arguments than those of overridden method
  13. matlab 非圆齿轮,非圆齿轮参数化设计关键技术研究
  14. iOS程序员为啥都爱用Mac电脑
  15. ActiveMQ学习笔记(二十三)Comsumer高级特性2
  16. 微信退款服务器系统失败怎么办,微信缴费失败怎么退款?能退回吗?
  17. win10服务器 稳定性,如何检测Win10系统稳定性?
  18. 宕机三个月、36亿打水漂,印度骄傲Infosys如何活成了全球笑话?
  19. (二)Redis数据类型和操作
  20. Unity初级工程师面试指导

热门文章

  1. 一些有用的Python问题
  2. webservice发送字符串
  3. 【2012天津区域赛】部分题解 hdu4431—4441
  4. .net之工作流工程展示及代码分享(二)工作流引擎
  5. flash模拟EEROM
  6. android各种权限及说明
  7. RHEL5(CentOS)下nginx+php+mysql+tomcat+memchached配置全过程(转)
  8. Rank() over()的用法
  9. valgrind——hisi平台valgrind
  10. OpenCV 【六】————youtu(图像)——旋转保存图片