本文是 Objective-C Runtime 系列文章的第三篇。如果你对 Objective-C Runtime 还不是很了解,可以先去看看前两篇文章:

  1. Objective-C Runtime
  2. Method Swizzling 和 AOP 实践

本篇会探究 KVO (Key-Value Observing) 实现机制,并去实践一番 - 利用 Runtime 自己动手去实现 KVO 。

KVO (Key-Value Observing)

KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

有意思的是,你不需要给被观察的对象添加任何额外代码,就能使用 KVO 。这是怎么做到的?

KVO 实现机制

KVO 的实现也依赖于 Objective-C 强大的 Runtime 。Apple 的文档有简单提到过 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 ...

Apple 的文档真是一笔带过,唯一有用的信息也就是:被观察对象的 isa 指针会指向一个中间类,而不是原来真正的类。看来,Apple 并不希望过多暴露 KVO 的实现细节。不过,要是你用 runtime 提供的方法去深入挖掘,所有被掩盖的细节都会原形毕露。Mike Ash 早在 2009 年就做了这么个探究。

简单概述下 KVO 的实现:

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。更具体的信息,去跑一下 Mike Ash 的那篇文章里的代码就能明白,这里就不再重复。

KVO 缺陷

KVO 很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。但官方实现的 KVO 提供的 API 实在不怎么样。

比如,你只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知。想要提供自定义的 selector ,不行;想要传一个 block ,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context 这个参数就是干这个的,也可以解决这个问题 - 在 -addObserver:forKeyPath:options:context: 传进去一个父类不知道的 context。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持 block 吧。

有不少人都觉得官方 KVO 不好使的。Mike Ash 的 Key-Value Observing Done Right,以及获得不少分享讨论的 KVO Considered Harmful 都把 KVO 拿出来吊打了一番。所以在实际开发中 KVO 使用的情景并不多,更多时候还是用 Delegate 或 NotificationCenter。

自己实现 KVO

如果没找到理想的,就自己动手做一个。既然我们对官方的 API 不太满意,又知道如何去实现一个 KVO,那就尝试自己动手写一个简易的 KVO 玩玩。

首先,我们创建 NSObject 的 Category,并在头文件中添加两个 API:

typedef void(^PGObservingBlock)(id observedObject, NSString *observedKey, id oldValue, id newValue);@interface NSObject (KVO)- (void)PG_addObserver:(NSObject *)observerforKey:(NSString *)keywithBlock:(PGObservingBlock)block;- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;@end

接下来,实现 PG_addObserver:forKey:withBlock: 方法。逻辑并不复杂:

  1. 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
  2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类;
  3. 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
  4. 添加这个观察者
- (void)PG_addObserver:(NSObject *)observerforKey:(NSString *)keywithBlock:(PGObservingBlock)block
{// Step 1: Throw exception if its class or superclasses doesn't implement the setterSEL setterSelector = NSSelectorFromString(setterForGetter(key));Method setterMethod = class_getInstanceMethod([self class], setterSelector);if (!setterMethod) {// throw invalid argument exception}Class clazz = object_getClass(self);NSString *clazzName = NSStringFromClass(clazz);// Step 2: Make KVO class if this is first time adding observer and //          its class is not an KVO class yetif (![clazzName hasPrefix:kPGKVOClassPrefix]) {clazz = [self makeKvoClassWithOriginalClassName:clazzName];object_setClass(self, clazz);}// Step 3: Add our kvo setter method if its class (not superclasses) //          hasn't implemented the setterif (![self hasSelector:setterSelector]) {const char *types = method_getTypeEncoding(setterMethod);class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);}// Step 4: Add this observation info to saved observation objectsPGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));if (!observers) {observers = [NSMutableArray array];objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}[observers addObject:info];
}

再来一步一步细看。

第一步里,先通过 setterForGetter() 方法获得相应的 setter 的名字(SEL)。也就是把 key 的首字母大写,然后前面加上 set 后面加上 :,这样 key 就变成了 setKey:。然后再用 class_getInstanceMethod 去获得 setKey: 的实现(Method)。如果没有,自然要抛出异常。

第二步,我们先看类名有没有我们定义的前缀。如果没有,我们就去创建新的子类,并通过 object_setClass() 修改 isa 指针。

- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];Class clazz = NSClassFromString(kvoClazzName);if (clazz) {return clazz;}// class doesn't exist yet, make itClass originalClazz = object_getClass(self);Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);// grab class method's signature so we can borrow itMethod 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;
}

动态创建新的类需要用 objc/runtime.h 中定义的 objc_allocateClassPair() 函数。传一个父类,类名,然后额外的空间(通常为 0),它返回给你一个类。然后就给这个类添加方法,也可以添加变量。这里,我们只重写了 class 方法。哈哈,跟 Apple 一样,这时候我们也企图隐藏这个子类的存在。最后 objc_registerClassPair() 告诉 Runtime 这个类的存在。

第三步,重写 setter 方法。新的 setter 在调用原 setter 方法后,通知每个观察者(调用之前传入的 block ):

static void kvo_setter(id self, SEL _cmd, id newValue)
{NSString *setterName = NSStringFromSelector(_cmd);NSString *getterName = getterForSetter(setterName);if (!getterName) {// throw invalid argument exception}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 *)(kPGKVOAssociatedObservers));for (PGObservationInfo *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);});}}
}

细心的同学会发现我们对 objc_msgSendSuper 进行类型转换。在 Xcode 6 里,新的 LLVM 会对 objc_msgSendSuper 以及 objc_msgSend 做严格的类型检查,如果不做类型转换。Xcode 会抱怨有 too many arguments 的错误。(在 WWDC 2014 的视频 What new in LLVM 中有提到过这个问题。)

最后一步,把这个观察的相关信息存在 associatedObject 里。观察的相关信息(观察者,被观察的 key, 和传入的 block )封装在 PGObservationInfo 类里。

@interface PGObservationInfo : NSObject@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PGObservingBlock block;@end

就此,一个基本的 KVO 就可以 work 了。当然,这只是一个一天多做出来的小东西,会有 bug,也有很多可以优化完善的地方。但作为 demo 演示如何利用 Runtime 动态创建类、如何实现 KVO,足已。

完整的例子可以从这里下载:ImplementKVO

如果有任何问题或找到 bug,可以邮件我 peng@glowing.com 或者私信我的微博@no_computer。

谢谢观赏。

Reference

KVO Implementation

Creating Classes at Runtime in Objective-C

Key-Value Observing Done Right

By your command

Associated Objects

原文地址:原文地址

KVO实现机制 如何自己动手实现 KVO相关推荐

  1. 深入理解 KVC\KVO 实现机制 — KVC

    KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing,所以本来想放在一起讲的.但是篇幅有限所以就分成了两篇博文 KVO实现机制传送门 KVC概述 KVC是Key Value Cod ...

  2. Attention使用注意力机制的seq2seq 动手学深度学习v2

    1. Attention使用注意力机制的seq2seq 2. Attention使用注意力机制的seq2seq 代码实现 3. Q&A attention在搜索的时候,是在当前句子搜索. 一般 ...

  3. ios kvo 要引入_iOS KVO 实现原理 和 自己实现KVO

    一:前言 KVO 是我们经常使用的键值观察者模式的一种实现 .大概功能是 比如有两个对象 A 和B  B 观察了A的某个属性E  ,当E发生变化的时候  B中收到回调 回调中 有新的 或者 旧的值 . ...

  4. iOS RAC 的使用总结 (转载)

    ReactiveCocoa(简称RAC),是GitHub上开源的一个应用于iOS和OS X开发的一个新框架.RAC具有函数式编程和响应者编程的特性. ReactiveCocoa解决的问题: 1.传统i ...

  5. 探究KVO的底层实现原理

    addObserver:forKeyPath:options:context:各个参数的作用分别是什么, observer中需要实现哪个方法才能获得KVO回调? /**1. self.person:要 ...

  6. 8 iOS中KVO 的本质

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

  7. iOS KVO 的实现原理

    KVO 的实现原理 一 原理 1.KVO是基于runtime机制实现的 2.当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的set ...

  8. zzz KVC/KVO原理详解及编程指南

    前言: 1.本文基本不讲KVC/KVO的用法,只结合网上的资料说说对这种技术的理解. 2.由于KVO内容较少,而且是以KVC为基础实现的,本文将着重介绍KVC部分. 一.简介 KVC/KVO是观察者模 ...

  9. KVC/KVO原理详解及编程指南

    作者:wangzz 原文地址:http://blog.csdn.net/wzzvictory/article/details/9674431 转载请注明出处 如果觉得文章对你有所帮助,请通过留言或关注 ...

最新文章

  1. Android studio 签名使用转
  2. 彻底搞懂JavaScript执行机制
  3. Java实现最小二乘法线性拟合,传感与检测,单臂半桥全桥实验,江南大学自动化
  4. ogre研究之第一个程序(一)
  5. (47)FPGA指数运算符(V代码实现)
  6. Network-based Fraud Detection for Social Security Fraud
  7. Python零基础入门(四)——Python面向对象编程[学习笔记]
  8. ES6中Number中的扩展
  9. yum list 报错linux,centos7下运行yum list 出现如下报错 求教!
  10. zuul网关,springsecurity认证中心 和 Swagger2 整合遇到的问题
  11. Java结构型设计模式之装饰者模式
  12. z17刷机miui12教程_努比亚Z17 MIUI12开发版 匠心独特 完美ROOT 隐私升级 通知栏升级...
  13. python编写登录脚本_python实现的登录和操作开心网脚本分享
  14. 尚德机构COO杜铮:在线教育进入下半场 释放供给侧红利成关键
  15. 计算机搜索不到网络打印机怎么处理,局域网内搜索不到打印机怎么办
  16. 知道创宇高级威胁情报团队:以APT测绘及APT防御应对高级威胁
  17. 散粉在哪个步骤用_定妆粉是在哪个步骤用
  18. extra argument in call
  19. STM32F103频率计
  20. vue-js实现日期加减,年月日,及单位换算。

热门文章

  1. 爬取链家网站的北京租房信息
  2. 尚硅谷web前端工程师1000集学习笔记11
  3. innosetup如何读取注册表及其字段值
  4. 基于wxpy的微信防撤回
  5. python手机编译器怎么编写程序_怎么用手机编写Python程序?
  6. python创建列表副本_解决Python使用列表副本的问题
  7. win7系统安装信息服务器不可用怎么办,RPC服务器不可用?Win7系统RPC服务器不可用怎么办...
  8. 使用迅搜制作站内搜索引擎-安装篇
  9. Java-Util之ArrayList
  10. 求约数java_最多约数问题(Java)