自己手动实现KVO

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

Objective-C Runtime

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:


1

2

3

4

5

6

7

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 方法;
  • 添加这个观察者

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

- (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 指针。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

- (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 ):


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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 类里。


1

2

3

4

5

@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,可以邮件我 [email protected] 或者私信我的微博 @no_computer

谢谢观赏。

时间: 2024-10-11 07:36:52

自己手动实现KVO的相关文章

手动实现KVO

前言 KVO(Key-Value Observing, 键值观察), KVO的实现也依赖于runtime. 当你对一个对象进行观察时, 系统会动态创建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. KVO还会修改原对象的isa指针指向这个新类. 我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象. 不仅如此, Apple

手动设定实例变量的KVO实现监听

如果将一个对象设定成属性,这个属性是自动支持KVO的,如果这个对象是一个实例变量,那么,这个KVO是需要我们自己来实现的. 以下给出源码供君测试: Student.h 与 Student.m // // Student.h // SuperNotification // // Copyright (c) 2014年 Y.X. All rights reserved. // #import <Foundation/Foundation.h> @interface Student : NSObje

iOS中的 观察者模式 之 KVO

1.KVO的简介 KVO 全称 Key-Value Observing.中文叫键值观察.KVO其实是一种观察者模式,观察者在键值改变时会得到通知,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身.相比Notification,KVO更加的简单直接. KVO的操作方法由NSKeyValueCoding提供,而他是NSObject的类别,也就是说ObjC中几乎所有的对象都支持KVO操作.  KVO 需要实现实例变量的 s

IOS SDK详解之KVO

原创Blog,转载请注明出处 blog.csdn.net/hello_hwc 前言:KVC和KVO是帮助我们驾驭objective C动态特性工具.KVO是建立在KVC基础上的,所以不了解KVC的同学可以参见我的这篇博客.这里我不会再重复讲解KVC. http://blog.csdn.net/hello_hwc/article/details/43769765 本文的内容 KVO的定义 KVO的典型使用场景. 手动KVO 几点KVO要说的地方 一 KVO的定义 KVO提供了一种key-value

KVO 使用及原理

KVO的基本原理大概是这样的   当一个对象被观察时, 系统会新建一个子类NSNotifying_A ,在子类中重写了对象被观察属性的 set方法,  并且改变了该对象的 isa 指针的指向(指向了新建的子类) , 当属性的值发生改变了, 会调用子类的set方法, 然后发出通知 一. KVO 的基本使用 给_person对象 添加观察者self, 当person对象的name的值发生改变的时候, 会触发observer方法 _person = [Person new]; p.name = @"o

通过子类实现KVO,浅析KVO底层原理

通过手动实现KVO,对KVO底层原理有一定认识. KVO只要是通过监听set方法,从而实现对该对象的监听. 要监听set方法,有两种实现方式,第一就是使用分类,重写set方法,但是这样就会覆盖父类的set方法,所以不可行,pass掉. 第二就是使用子类,把父类的isa指针改为子类.然后调用父类色set方法,最后调用回调方法,该方案可行. 首先是注册监听,在调用监听方法的时候,会动态实现子类,把observer保存到子类的属性中(弱引用weak类型,不能使用strong,会造成循环引用),并且把类

iOS 并发编程之 Operation Queues

http://blog.leichunfeng.com/blog/2015/07/29/ios-concurrency-programming-operation-queues/ 现如今移动设备也早已经进入了多核心 CPU 时代,并且随着时间的推移,CPU 的核心数只会增加不会减少.而作为软件开发者,我们需要做的就是尽可能地提高应用的并发性,来充分利用这些多核心 CPU 的性能.在 iOS 开发中,我们主要可以通过 Operation Queues.Dispatch Queues 和 Dispa

深度理解Key-Value Observing 键值观察

前言   在上一阶段的开发过程中,我们大量使用了 KVO 机制,来确保页面信息的及时同步.也因此碰到了很多问题,促使我们去进一步学习 KVO 的相关机制,再到寻找更好的解决方案.鉴于 KVO 让人欲仙欲死的使用经历,在这里做一个简单分享.此分享的目的,更多的是在于点出 KVO 相关的技术点,供我们大家在学习和使用过程中做一个参考. 对于 KVO 的背后机制感兴趣的同学,可以直接看第三部分,KVC 和 isa-swizzling . 对于 替代方案感兴趣的同学,请直接跳到末尾的第五部分,有列出了目

struct和class的区别 观察者模式 https连接 点击button收到点击事件,中间发生了什么

问题: 4道过滤菜鸟的iOS面试题 网上已经有很多针对各种知识点的面试题,面试时有些人未必真正理解也能通过背题看上去很懂.我自己总结了4道面试题,好快速的判断这个人是否是一个合格的工程师,欢迎大家点评. 1.struct和class的区别 在面试之前你觉得所有的计算机专业的学生都应该能答的上来,事实是我面的人里有超过三分一没有答上来. 有时我还会顺便问下swfit里的array是什么类型,在大量复制时会不会有性能问题. 2.介绍一下观察者模式 也许有些人已经觉得设计模式有些过时,没有整本读过.就