前言

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

手动实现KVO的更多相关文章

  1. 手动实现 KVO

    来源:伯乐在线 - Jerry4me 链接:http://ios.jobbole.com/88828/ 点击 → 申请加入伯乐在线专栏作者 我的Github地址 : https://github.co ...

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

    手动设定实例变量的KVO实现监听 如果将一个对象设定成属性,这个属性是自动支持KVO的,如果这个对象是一个实例变量,那么,这个KVO是需要我们自己来实现的. 以下给出源码供君测试: Student.h ...

  3. 深入剖析通知中心和KVO

    深入剖析通知中心和KVO 要先了解KVO和通知中心,就得先说说观察者模式,那么观察者模式到底是什么呢?下面来详细介绍什么是观察者模式. 观察者模式 -A对B的变化感兴趣,就注册成为B的观察者,当B发生 ...

  4. OC 观察者模式(通知中心,KVO)

    OC 观察者模式(通知中心,KVO) 什么是观察者模式??? A对B的变化感兴趣,就注册为B的观察者,当B发生变化时通知A,告知B发生了变化.这就是观察者模式. 观察者模式定义了一种一对多的依赖关系, ...

  5. KVC与KVO的进阶使用

    本篇主要介绍键-值编码KVC,键值观察KVO的进阶使用的一些技巧主要是一下两个方面: KVC的集合操作符 KVO的手动实现方式 KVC集合操作符 关于集合操作符在苹果官方文档搜索Collection ...

  6. ios - 再细读KVO

    [罗国强原创] KVO - Key-Value Observing. 它提供了一种机制,允许对象被通知到其他对象的具体特性的变化.它特别适用于一个应用的模型层与控制层的交互. 一种典型的应用场景是在一 ...

  7. kvo观察实例变量

    // 手动设定KVO - (void)setAge:(NSString *)age { [self willChangeValueForKey:@"age"]; _age = ag ...

  8. 【OC底层】KVO原理

    KVO的原理是什么?底层是如何实现的? KVO是Key-value observing的缩写. KVO是Objective-C是使用观察者设计模式实现的. Apple使用了isa混写(isa-swiz ...

  9. kvo本质探寻

    一.概述 1.本文章内容,须参照本人的另一篇博客文章“class和object_getClass方法区别”加以理解: 2.基本使用: //给实例对象instance添加观察者,监听该实例对象的某个属性 ...

随机推荐

  1. thinkphp的自动完成功能说明

    手册里有一句话很关键: 自动完成是ThinkPHP提供用来完成数据自动处理和过滤的方法,使用create方法创建数据对象的时候会自动完成数据处理. 这句话说明自动完成发生的时间是create()组建数 ...

  2. 最近用到这个强大的工具 PhysicsEditor (转)

    今天收到PhysicsEditor作者发过来的license key,所以顺便把PhysicsEditor也尝试了一下.主要是尝试将PhysicsEditor与cocos2dx,box2d结合开发的一 ...

  3. 关于ListView嵌套GridView中的onItemClickListener失效问题

    一开始在ListView中设置了onItemClickListener,在里面Log输出Item列表的位置,完全没有反应, 网上大部分说的什么把子组件屏蔽掉(而且好多都是转载的一样的), 可是我希望的 ...

  4. ZOJ-2338 The Towers of Hanoi Revisited 输出汉诺塔的最优解移动过程

    题意:给定N(1<= N <=64)个盘子和M(4<= M <= 65)根柱子,问把N个盘子从1号柱子移动到M号柱子所需要的最少步数,并且输出移动过程. 分析:设f[i][j] ...

  5. 九度-剑指Offer

    二维数组中的查找 分析:既然已经给定了每一行从左至右递增,那么对于每一行直接二分查找即可,一开始还想着每一列同样查找一次,后来发现每一行查找一遍就能够遍历所有的元素了. #include <cs ...

  6. 解耦HTML、CSS和JavaScript

    当前在互联网上,任何一个稍微复杂的网站或者应用程序都会包含许多HTML.CSS和JavaScript.随着互联网运用的发展以及我们对它的依赖性日益增加,设定一个关于组织和维护你的前端代码的计划是绝对需 ...

  7. poj3667【线段树】/【类似权值线段树写法】

    题意:n个空房间.两种操作:1.选择最小的连续D个房间入住,并输出这连续D个房间的最小标号.2.将某个区间内的房间全部退房. #include <cstdio> #include < ...

  8. poj1673EXOCENTER OF A TRIANGLE

    链接 据说这题是垂心..数学太弱没有看出来,写了分朴实无华的代码.. 旋转三边得到图中的外顶点,然后连接三角形顶点求交点,交上WA..觉得没什么错误就去看了下discuss,发现都在说精度问题,果断开 ...

  9. android平台的技术架构

    Android平台采用了软件堆层(Software Stack)的架构,主要分为四个部分: 1.应用软件 Android 连同一个核心应用程序包一起发布,该应用程序包包括E-mail客户端.SMS短消 ...

  10. asc.desc

    DESC 是descend 降序意思 asc 是ascend 升序的意思