iOS-手动实现KVO

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


前言

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参数的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

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

[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 cheap, show you the code!


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

NSObject+jr_KVO.h

#import <Foundation/Foundation.h>
#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<JRObserverInfo *> *)
- (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];
    
}

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

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

- (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:方法了

- (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实现也很简单.

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也正正是在这里体现出来的.

    /**
     *  重写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的区别 :

Apple文档中是这么说的 : 
void objc_msgSend(void /* id self, SEL op, ... */)
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */)

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

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

objc_msgSendSuper(&superClazz, _cmd, newValue);
// 当你这样做时, 编译器会报以下错误
/* Too many arguments to function call, expected 0, have 3 */
// 所以我们需要做个方法类型的强转, 就不会报错了

移除监听者

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

- (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是个什么模型呢? 这里告诉大家...

// 回调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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 前言 KVO:简单的来说,就是观察者观察被观察对象属性的变化而发生相应的变化。实现的原理基于KVC与强大的Runt...
    Colleny_Z阅读 1,274评论 0 2
  • 本篇会对KVO的实现进行探究,不涉及太多KVO的使用方法,但是会有一些使用时的思考。 一、使用上的疑问 1.key...
    奋拓达阅读 499评论 0 2
  • 寻秦记 1 旅客们步履匆忙走出车站,绕过灰色城墙,消失在车流人海中。他们脚步带起的微尘还在身后升腾。这是西安的尘土...
    九曲胡同阅读 939评论 9 8
  • 早上醒来,舍友A问我:“你还要一个人啊”?我一头雾水,是在跟我说话吗? A小姐一脸嫌弃:“今天是520,大姐。” ...
    会飞的小蜗牛阅读 502评论 0 5