手动实现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
方法里我们需要做什么呢?
- 检查对象是否存在该属性的setter方法, 没有的话我们就做什么都白搭了, 既然别人都不允许你修改值了, 那也就不存在监听值改变的事了
- 检查自己(self)是不是一个kvo_class(如果该对象不是第一次被监听属性, 那么它就是kvo_class, 反之则是原class), 如果是, 则跳过这一步; 如果不是, 则要修改self的类(origin_class -> kvo_class)
- 经过第二部, 到了这里已经100%确定self是kvo_class的对象了, 那么我们现在就要重写kvo_class对象的对应属性的setter方法
- 最后, 将观察者对象(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_msgSend
与objc_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
手动实现KVO的更多相关文章
- 手动实现 KVO
来源:伯乐在线 - Jerry4me 链接:http://ios.jobbole.com/88828/ 点击 → 申请加入伯乐在线专栏作者 我的Github地址 : https://github.co ...
- 手动设定实例变量的KVO实现监听
手动设定实例变量的KVO实现监听 如果将一个对象设定成属性,这个属性是自动支持KVO的,如果这个对象是一个实例变量,那么,这个KVO是需要我们自己来实现的. 以下给出源码供君测试: Student.h ...
- 深入剖析通知中心和KVO
深入剖析通知中心和KVO 要先了解KVO和通知中心,就得先说说观察者模式,那么观察者模式到底是什么呢?下面来详细介绍什么是观察者模式. 观察者模式 -A对B的变化感兴趣,就注册成为B的观察者,当B发生 ...
- OC 观察者模式(通知中心,KVO)
OC 观察者模式(通知中心,KVO) 什么是观察者模式??? A对B的变化感兴趣,就注册为B的观察者,当B发生变化时通知A,告知B发生了变化.这就是观察者模式. 观察者模式定义了一种一对多的依赖关系, ...
- KVC与KVO的进阶使用
本篇主要介绍键-值编码KVC,键值观察KVO的进阶使用的一些技巧主要是一下两个方面: KVC的集合操作符 KVO的手动实现方式 KVC集合操作符 关于集合操作符在苹果官方文档搜索Collection ...
- ios - 再细读KVO
[罗国强原创] KVO - Key-Value Observing. 它提供了一种机制,允许对象被通知到其他对象的具体特性的变化.它特别适用于一个应用的模型层与控制层的交互. 一种典型的应用场景是在一 ...
- kvo观察实例变量
// 手动设定KVO - (void)setAge:(NSString *)age { [self willChangeValueForKey:@"age"]; _age = ag ...
- 【OC底层】KVO原理
KVO的原理是什么?底层是如何实现的? KVO是Key-value observing的缩写. KVO是Objective-C是使用观察者设计模式实现的. Apple使用了isa混写(isa-swiz ...
- kvo本质探寻
一.概述 1.本文章内容,须参照本人的另一篇博客文章“class和object_getClass方法区别”加以理解: 2.基本使用: //给实例对象instance添加观察者,监听该实例对象的某个属性 ...
随机推荐
- ERROR 1044 (42000): Access denied for user 'root'@'localhost' to database 'mysql'
mysql> use mysqlERROR 1044 (42000): Access denied for user 'root'@'localhost' to database 'mysql' ...
- UML分析与设计
考点: 掌握面向对象的分析与设计 掌握UML描述方法 用例图.类图.序列图.状态转换图 类图:类的属性.方法的识别:类间的各种关系 类图:实体.联系 各种关系图例: 泛化:取公共属性 关联分为聚合.组 ...
- ABAP断点调试
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 常用的STL查找算法
常用的STL查找算法 <effective STL>中有句忠告,尽量用算法替代手写循环:查找少不了循环遍历,在这里总结下常用的STL查找算法: 查找有三种,即点线面: 点就是查找目标为单个 ...
- JS学习笔记(三) 对象
参考资料: 1. http://www.w3school.com.cn/js/js_objects.asp ☂ 知识点: ☞ Javascript中的所有事物都是对象. ☞ Javascript是基于 ...
- 保留ip: Reserved IP addresses
Reserved IP addresses From Wikipedia, the free encyclopedia In the Internet addressing architect ...
- LotteryDrawing
import java.util.*; public class MyTest{ public static void main(String[] args){ Scanner in = new Sc ...
- mysql 内连接 左连接 右连接 外连接
mysql> desc student;+-------+-------------+------+-----+---------+-------+| Field | Type | Null | ...
- asp.net实现大文件上传
需要下载NeatUpload插件 上传页面: <%@ Page Language="C#" AutoEventWireup="true" CodeFile ...
- (一)SecureCRT连接虚拟机linux
最近在学习linux,在使用SecureCRT连接虚拟机linux时遇到了一些问题,现在总结一下. 1.首先要配置linux配置文件,修改静态IP地址以及掩码,保持与本地在同一网段.更改配置文件方法如 ...