手动实现KVO


前言

KVO(Key-Value Observing, 键值观察), KVO的实现也依赖于runtime. 当你对一个对象进行观察时, 系统会动态创建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. KVO还会修改原对象的isa指针指向这个新类.

我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.

不仅如此, Apple还重写了原类的- class方法, 视图欺骗我们, 这个类没有变, 还是原来的那个类(偷龙转凤). 只要我们懂得Runtime的原理, 这一切都只是掩耳盗铃罢了.

以下实现是参考Glow 技术团队博客的文章进行修改而成, 主要目的是加深对runtime的理解, 大家看完后不妨自己动手实现以下, 学而时习之, 不亦乐乎


KVO的缺陷

Apple给我们提供的KVO不能通过block来回调处理, 只能通过下面这个方法来处理, 如果监听的属性多了, 或者监听了多个对象的属性, 那么这里就痛苦了, 要一直判断判断if else if else….多麻烦啊, 说实话我也不懂为什么Apple不提供多一个传block参数的方法

Objective-C

1

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

那么, 既然Apple没有帮我们实现, 那我们就手动实现一个呗, 先看下我们最终目标是什么样的 :

Objective-C

1

2

3

4

5

6

[object jr_addObserver:observer key:@"name" callback:^(id observer, NSString *key, id oldValue, id newValue) {

// do something here

}];

[object jr_addObserver:observer key:@"address" callback:^(id observer, NSString *key, id oldValue, id newValue) {

// do something here

}];

简简单单就能让observer监听object的两个属性, 并且监听属性改变后的回调就在对应的callback下, 清晰明了, 何不快哉! Talk is cheep, show you the code!



首先, 我们为NSObject新增一个分类

NSObject+jr_KVO.h

Objective-C

1

2

3

4

5

6

7

8

9

10

#import

#import "JRObserverInfo.h"

@interface NSObject (jr_KVO)

- (void)jr_addObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback;

- (void)jr_removeObserver:(id)observer key:(NSString *)key;

@end

添加观察者

jr_addObserver方法里我们需要做什么呢?

  1. 检查对象是否存在该属性的setter方法, 没有的话我们就做什么都白搭了, 既然别人都不允许你修改值了, 那也就不存在监听值改变的事了
  2. 检查自己(self)是不是一个kvo_class(如果该对象不是第一次被监听属性, 那么它就是kvo_class, 反之则是原class), 如果是, 则跳过这一步; 如果不是, 则要修改self的类(origin_class -> kvo_class)
  3. 经过第二部, 到了这里已经100%确定self是kvo_class的对象了, 那么我们现在就要重写kvo_class对象的对应属性的setter方法
  4. 最后, 将观察者对象(observer), 监听的属性(key), 值改变时的回调block(callback), 用一个模型(JRObserverInfo)存进来, 然后利用关联对象维护self的一个数组(NSMutableArray *)

Objective-C

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

34

35

36

37

38

39

40

41

42

43

- (void)jr_addObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback

{

// 1. 检查对象的类有没有相应的 setter 方法。如果没有抛出异常

SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);

Method setterMethod = class_getInstanceMethod([self class], setterSelector);

if (!setterMethod) {

NSLog(@"找不到该方法");

// throw exception here

}

// 2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类

Class clazz = object_getClass(self);

NSString *className = NSStringFromClass(clazz);

if (![className hasPrefix:JRKVOClassPrefix]) {

clazz = [self jr_KVOClassWithOriginalClassName:className];

object_setClass(self, clazz);

}

// 到这里为止, object的类已不是原类了, 而是KVO新建的类

// 例如, Person -> JRKVOClassPrefixPerson

// JRKVOClassPrefix是一个宏, = @"JRKVO_"

// 3. 为kvo class添加setter方法的实现

const char *types = method_getTypeEncoding(setterMethod);

class_addMethod(clazz, setterSelector, (IMP)jr_setter, types);

// 4. 添加该观察者到观察者列表中

// 4.1 创建观察者的信息

JRObserverInfo *info = [[JRObserverInfo alloc] initWithObserver:observer key:key callback:callback];

// 4.2 获取关联对象(装着所有监听者的数组)

NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey);

if (!observers) {

observers = [NSMutableArray array];

objc_setAssociatedObject(self, JRAssociateArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

[observers addObject:info];

}

这段代码还有几个方法, 我们下面一一解释…

首先, setterForGetter 和 getterForSetter, 这两个方法好办. 第一个就是根据getter方法名获得对应的setter方法名, 第二个就是根据setter方法名获得对应的getter方法名

Objective-C

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

34

- (NSString *)setterForGetter:(NSString *)key

{

// name -> Name -> setName:

// 1. 首字母转换成大写

unichar c = [key characterAtIndex:0];

NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]];

// 2. 最前增加set, 最后增加:

NSString *setter = [NSString stringWithFormat:@"set%@:", str];

return setter;

}

- (NSString *)getterForSetter:(NSString *)key

{

// setName: -> Name -> name

// 1. 去掉set

NSRange range = [key rangeOfString:@"set"];

NSString *subStr1 = [key substringFromIndex:range.location + range.length];

// 2. 首字母转换成大写

unichar c = [subStr1 characterAtIndex:0];

NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]];

// 3. 去掉最后的:

NSRange range2 = [subStr2 rangeOfString:@":"];

NSString *getter = [subStr2 substringToIndex:range2.location];

return getter;

}

这里需要注意的是, 首字母转换成大写这一项, 不能直接调用NSString的capitalizedString方法, 因为该方法返回的是除了首字母大写之外其他字母全部小写的字符串.

然后, 接下来就是jr_KVOClassWithOriginalClassName:方法了

Objective-C

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

- (Class)jr_KVOClassWithOriginalClassName:(NSString *)className

{

// 生成kvo_class的类名

NSString *kvoClassName = [JRKVOClassPrefix stringByAppendingString:className];

Class kvoClass = NSClassFromString(kvoClassName);

// 如果kvo class已经被注册过了, 则直接返回

if (kvoClass) {

return kvoClass;

}

// 如果kvo class不存在, 则创建这个类

Class originClass = object_getClass(self);

kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);

// 修改kvo class方法的实现, 学习Apple的做法, 隐瞒这个kvo_class

Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));

const char *types = method_getTypeEncoding(clazzMethod);

class_addMethod(kvoClass, @selector(class), (IMP)jr_class, types);

// 注册kvo_class

objc_registerClassPair(kvoClass);

return kvoClass;

}

这个方法还是很直观明了的, 可能不太明白的是为什么要为kvo_class这个类重写class方法呢? 原因是我们要把这个kvo_class隐藏掉, 让别人觉得自己的类没有发生过任何改变, 以前是Person, 添加观察者之后还是Person, 而不是KVO_Person.
这个jr_class实现也很简单.

Objective-C

1

2

3

4

5

6

Class jr_class(id self, SEL cmd)

{

Class clazz = object_getClass(self); // kvo_class

Class superClazz = class_getSuperclass(clazz); // origin_class

return superClazz; // origin_class

}

最后, 重头戏来了, 那就是重写kvo_class的setter方法! Observing也正正是在这里体现出来的.

Objective-C

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

34

35

36

37

38

/**

*  重写setter方法, 新方法在调用原方法后, 通知每个观察者(调用传入的block)

*/

static void jr_setter(id self, SEL _cmd, id newValue)

{

NSString *setterName = NSStringFromSelector(_cmd);

NSString *getterName = [self getterForSetter:setterName];

if (!getterName) {

NSLog(@"找不到getter方法");

// throw exception here

}

// 获取旧值

id oldValue = [self valueForKey:getterName];

// 调用原类的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);

// 为什么不能用下面方法代替上面方法?

//    ((void (*)(id, SEL, id))objc_msgSendSuper)(self, _cmd, newValue);

// 找出观察者的数组, 调用对应对象的callback

NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey);

// 遍历数组

for (JRObserverInfo *info in observers) {

if ([info.key isEqualToString:getterName]) {

// gcd异步调用callback

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

info.callback(info.observer, getterName, oldValue, newValue);

});

}

}

}

卧槽, struct objc_super是什么玩意, 卧槽, ((void (*)(void *, SEL, id))objc_msgSendSuper)(&superClazz, _cmd, newValue);这一大串又是什么玩意???

?????

首先, 我们来看看objc_msgSendobjc_msgSendSuper的区别 :

Objective-C

1

2

3

Apple文档中是这么说的 :

void objc_msgSend(void /* id self, SEL op, ... */)

void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */)

那么, 很显然, 我们调用objc_msgSendSuper的时候, 第一个参数已经不一样了, 他接受的是一个指向结构体的指针, 于是才有了我们上面废力气创建的一个看似无用结构体

另外, 调用objc_msgSend总是需要做方法的类型强转,

Objective-C

1

2

3

4

objc_msgSendSuper(&superClazz, _cmd, newValue);

// 当你这样做时, 编译器会报以下错误

/* Too many arguments to function call, expected 0, have 3 */

// 所以我们需要做个方法类型的强转, 就不会报错了


移除监听者

移除监听者就easy easy easy太多了, 直接上代码吧

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

- (void)jr_removeObserver:(id)observer key:(NSString *)key

{

NSMutableArray *observers = objc_getAssociatedObject(self, JRAssociateArrayKey);

if (!observers) return;

for (JRObserverInfo *info in observers) {

if([info.key isEqualToString:key]) {

[observers removeObject:info];

break;

}

}

}

相信不用注释大家也能看懂, 大家记得在对象- dealloc方法中调用该方法移除监听者就OK了, 否则有可能报野指针错误, 访问坏内存.


监听者信息

JRObserverInfo是个什么模型呢? 这里告诉大家…

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

// 回调block大家可以自行定义

typedef void (^JRKVOCallback)(id observer, NSString *key, id oldValue, id newValue);

@interface JRObserverInfo : NSObject

/** 监听者 */

@property (nonatomic, weak) id observer;

/** 监听的属性 */

@property (nonatomic, copy) NSString *key;

/** 回调的block */

@property (nonatomic, copy) JRKVOCallback callback;

- (instancetype)initWithObserver:(id)observer key:(NSString *)key callback:(JRKVOCallback)callback;

@end


运行展示

这里我就简单做个展示, 下面的textLabel监听上面colorView背景色的改变, 点击button, 改变上面colorView的颜色, 然后textLabel输出colorView的当前色

运行结果



demo可在JRCustomKVODemo这里下载, 同时欢迎大家关注我的Github, 觉得有帮助的话还请给个star~~



参考 :
如何自己动手实现KVO

本文作者: 伯乐在线 - Jerry4me 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者

我的Github地址 : Jerry4me, 本文章的demo链接 : JRCustomKVODemo

时间: 2024-10-24 09:58:06

手动实现KVO的相关文章

自己手动实现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 对观察者模式(Obse

手动设定实例变量的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.介绍一下观察者模式 也许有些人已经觉得设计模式有些过时,没有整本读过.就