如何自己动手实现 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 *)observer
forKey:(NSString *)key
withBlock:(PGObservingBlock)block;
- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;
@end
接下来,实现 PG_addObserver:forKey:withBlock: 方法。逻辑并不复杂:
- 检查对象的类有没有相应的 setter 方法。如果没有抛出异常;
- 检查对象
isa指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把isa指向这个新建的子类; - 检查对象的 KVO 类重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
- 添加这个观察者
- (void)PG_addObserver:(NSObject *)observer
forKey:(NSString *)key
withBlock:(PGObservingBlock)block
{
// Step 1: Throw exception if its class or superclasses doesn't implement the setter
SEL 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 yet
if (![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 setter
if (![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 objects
PGObservationInfo *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 it
Class originalClazz = object_getClass(self);
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
// grab class method's signature so we can borrow it
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;
}
动态创建新的类需要用 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 complain
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super's setter, which is original class's setter method
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// look up observers and call the blocks
NSMutableArray *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
Creating Classes at Runtime in Objective-C
Key-Value Observing Done Right
如何自己动手实现 KVO(转)的更多相关文章
- 手动实现KVO
前言 KVO(Key-Value Observing, 键值观察), KVO的实现也依赖于runtime. 当你对一个对象进行观察时, 系统会动态创建一个类继承自原类, 然后重写被观察属性的sette ...
- 手动实现 KVO
来源:伯乐在线 - Jerry4me 链接:http://ios.jobbole.com/88828/ 点击 → 申请加入伯乐在线专栏作者 我的Github地址 : https://github.co ...
- 关于KVO导读
入门篇 KVO是什么? Key-value observing is a mechanism that allows objects to be notified of changes to spec ...
- IOS-详解KVO底层实现
一.KVO (Key-Value Observing) KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现.也是 Cocoa Binding 的基础.当被观察对 ...
- 对于KVO,你真的了解么?
目录 关于面试 官方文档 核心代码 (Key-Value Observing) 进阶(手动创建KVO) 关于isa指针 参考文章链接 一.关于面试 面试官:谈一谈你对KVO的理解? A:添加响应者 ...
- iOS面试题
一个区分度很大的面试题 考察一个面试者基础咋样,基本上问一个 @property 就够了: @property 后面可以有哪些修饰符? 线程安全的: atomic,nonatomic 访问权限的 re ...
- 我收录整理的优秀OC技术类文章
自定义导航按钮UIBarButtonItem 关于导航栏的六个小技巧 ios开发的一些小技巧篇一 制作一个可以滑动操作的 Table View Cell - IOS - 伯乐在线 一个 ...
- supersr--KVO/KVC
KVO内部实现原理 1.KVO是基于runtime机制实现的. 2.当某个类的对象第一次被观察是,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的set方法.派生类 ...
- 李洪强iOS经典面试题下
李洪强iOS经典面试题下 21. 下面的代码输出什么? @implementation Son : Father - (id)init { self = [super init]; if (self) ...
随机推荐
- jmeter 响应结果分析二
转自:http://www.cnblogs.com/Carrie_Liang/archive/2008/11/10/1330997.html 前文再续,续接上一回.上一篇讲了如何利用Assertion ...
- thinkphp 定制错误页面
在前台配置文件里加上: 'TMPL_EXCEPTION_FILE' => '.Public/tpl/error.html',// 异常cuowu页面的模板文件 然后在Public下新建一个tpl ...
- 【转】VMware设置共享文件夹之后Ubuntu中看不到怎么办?
一.共享文件夹设置好了,但是在虚拟机中的Ubuntu系统下却看不到,怎么办? 一种可能的原因是系统没有自动挂载,解决办法: 1.安装: sudo apt-get insta ...
- 分布式环境下的id生成方法
分布式环境下的id生成方法 前几天研究数据库分表分库的问题,其中有一个关键的地方就是生成唯一键的问题,假如数据表有1亿条数据,而且还在不断的增加,这里我们就需要考虑到分表分库,假设我们采用Hash ...
- PHP使用Mysql事务实例解析
<?php //数据库连接 $conn = mysql_connect('localhost', 'root', ''); mysql_select_db('test', $conn); mys ...
- cadence allegro PCB中怎么使查找元件时屏幕不移动
先按F4进入show element状态,你在找元件,屏幕就会移动到你找的元件上面去,并且将视图放大.
- dom解析器机制 web基本概念 tomcat
0 作业[cn.itcast.xml.sax.Demo2] 1)在SAX解析器中,一定要知道每方法何时执行,及SAX解析器会传入的参数含义 1 理解dom解析器机制 1)dom解析和dom4j原理 ...
- 深入解析字符串的比较方法:“==”操作符;String.Equals方法;String.Compare方法;String.CompareOrdinal方法。
1:要判断2个字符串变量是否相等,最高效的方法是看它们是否指向相同的内存地址.前面使用RefernceEquals方法来比较.如果2个变量指向的是不同的内存地址,那么就需要逐字符的比较2个字符串的变量 ...
- Hibernate数据库持久层框架
Hibernate是一种Java语言下的对象关系映射解决方案. 它是使用GNU宽通用公共许可证发行的自由.开源的软件.它为面向对象的领域模型到传统的关系型数据库的映射,提供了一个使用方便的框架.Hib ...
- Codeforces 374A - Inna and Pink Pony
原题地址:http://codeforces.com/contest/374/problem/A 好久没写题目总结了,最近状态十分不好,无论是写程序还是写作业还是精神面貌……NOIP挂了之后总觉得缺乏 ...