KVO 探究(一)自己实现KVO

何为KVO

KVO 是 key value observing 的简写:
苹果官方文档的解释:Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO的意义

KVO 是应对观察者模式而产生的,当被观察者发生相应的改变,观察者对象会得到一个通知。

KVO 是如何实现的

KVO 运用的是runtime来进行处理的。当我们观察一个对象的时候,一个新类会被动态的创建。这个新类继承自原来的对象的类。继承之后苹果重写了被观察属性的setter方法。苹果会在调用原来的setter方法的时候通知所观察对象发生了改变。然后将这个对象的isa指向了新创建的类,这个对象就变成了新创建的子类的对象。除此之外,苹果还偷天换日重写了-class方法,让我们觉得这个类并没有改变。

苹果的描述

Automatic key-value observing is implemented using a techique called isa-swizzling ... when an observer is registerd for an atttribute of object the isa pointer of the observed object is modified,pointing to an intermediate class rather than at the true class...

KVO的问题

  1. -addObserver:forKeyPath:options:context (dosen't allow passing a custom selector to be invoked。---- kvo不允许我们添加block 和 selector。暴露出来的api 比较鸡肋。
  2. 我们纵观NSNotificationCenter 我们就能了解KVO 是可以进行方法的指定的;

KVO的用法

给某个对象的某个属性(可以是基本数据类型,也可以是对象类型)添加一个,观察者,我们观察的属性行为有啥(new ,old ,即为NSKeyValueChangeKey)。
//注册:
[self addObserver:self forKeyPath:@"tempt" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
//对应要取消 ,在注册类的析构函数里面进行取消
removeObserver: forKeyPath:
//观察方法
observeValueForKeyPath:ofObject:object change:change context:

问题

我们观察属性一般发生了变化就要在观察方法中进行相应的业务逻辑的处理,我们将业务逻辑放到下面是不是不太聚合业务。我们有没有什么办法,在注册的时候直接就可以看到业务逻辑,而不用还要往下翻去找这个函数再看业务逻辑呢?当然有,聚合在一起我们首先就会想到一个神奇的东西,代码块,在iOS中即为Block。

如何手动实现KVO

思路:KVO实现的原理是,当我们监听一个对象的某个属性的时候,就会产生一个类的子类,然后对这个类被监听的属性进行setter方法的重写(要考虑这个这个方法有没有setter方法)。然后我们再将对象的isa指针指向这个子类。最后我们再来一步,偷天换日,重写class 类的方法。 最重要一点 千万要remove observer

具体思路

  1. 看setter方法有没有,没有则返回,抛出异常。(KVO的本质是改写setter方法,在里面添加修改属性的出发方法)
  2. 类是否为KVO类,不是就新建子类,将isa 指向这个类。
  3. KVO 是否重写setter方法,没有就添加
    4.保存信息,留作setter触发方法的时候使用。

所需知识点

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

开始撸

设计接口

@interface NSObject (LBKVO)

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

//接口设计 ,需要什么属性呢? 观察者,被观察者的属性,对应条件下发生的改变的回调。
- (void)LB_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath changeBlock:(LBKVOBlock)changeBlock;

- (void)LB_removeObserver:(NSObject *)observer keyPath:(NSString *)keyPath;

@end

实现

  1. 看setter方法有没有,没有则返回,抛出异常。(KVO的本质是改写setter方法,在里面添加修改属性的出发方法)
#pragma mark - 检查是否有设置方法
- (Method)p_checkIsImplementionSetterWithContext:(NSObject *)context keypath:(NSObject *)keypath{
    //get method
    SEL selector = NSSelectorFromString(setterForGetter(keypath));
    Method setMethod = class_getInstanceMethod([self class], selector);
    return setMethod;
}

  1. 类是否为KVO类,不是就新建子类,将isa 指向这个类。
 //2. 判断这个类是否是自定义KVO类
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    //3. 没有就创建一个类并且让该对象的isa指向
    if (![clazzName hasPrefix:kLBKVOPrefix]) {
        clazz = [self createKVOClassWithClassName:clazzName];
        object_setClass(self, clazz); //指向这个类
    }

2.1 如何创建一个子类呢?

- (Class)createKVOClassWithClassName:(NSString *)className{
    NSString *kvoClazzName = [kLBKVOPrefix stringByAppendingString:className];
    Class KVOClazz = NSClassFromString(kvoClazzName);
    if (KVOClazz) {
        return KVOClazz;
    }
    //创建
    Class superClazz = object_getClass(self);
    Class KVOClazz1 = objc_allocateClassPair(superClazz, kvoClazzName.UTF8String, 0);
 
    objc_registerClassPair(KVOClazz1);
    return KVOClazz1;
}

2.2 那么我们如何进行偷天换日呢?

    //获得Types类型 ,一定要在registerClassPair方法前进行替换
    Method m = class_getInstanceMethod(superClazz, @selector(class));
    const char *types = method_getTypeEncoding(m);
    class_addMethod(KVOClazz1, @selector(class), (IMP)LBKVO_Class, types);
  1. 重写setter方法
  //重写setter方法
 const char * types = method_getTypeEncoding(superSetMethod);
 class_addMethod(object_getClass(self), NSSelectorFromString(setterForGetter(keyPath)), (IMP)KVO_setterIMP, types);

重点来了

如何setter方法如何进行重写呢

static void KVO_setterIMP(id self,SEL _cmd,id newValue){
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
  
    id oldValue = [self valueForKey:getterName];
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    void (*objc_msgSendSuperCasted)(void *,SEL ,id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superClazz,_cmd,newValue);
    NSMutableArray<LBKVOInfo *> *observers = objc_getAssociatedObject(self, (__bridge const void *)kLBKVOObserversKey);
    [observers enumerateObjectsUsingBlock:^(LBKVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([(NSString *)obj.key isEqualToString:getterName]) {
            obj.changeBlock(self, getterName, oldValue, newValue);
        }
    }];

}

4.进行信息的保存

   NSMutableArray *observers = objc_getAssociatedObject(self,(__bridge const void *) kLBKVOObserversKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *) kLBKVOObserversKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    LBKVOInfo *info = [[LBKVOInfo alloc] initWithObserver:observers key:keyPath changeBlock:changeBlock];
    [observers addObject:info];

组装

将1,2,3,4逻辑进行组装,就是KVO实现的核心逻辑了。

调用

[self LB_addObserver:self keyPath:@"tempt" changeBlock:^(id  _Nonnull observer, NSString * _Nonnull key, id  _Nonnull oldValue, id  _Nonnull newValue) {
         NSLog(@"sadf");
}];

别忘记remove

-(void)dealloc{
    [self LB_removeObserver:self keyPath:@"tempt"];
}

Demo 传送门: https://github.com/LeonLeeboy/LBKVO

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,101评论 1 32
  • KVO原理浅析 KVO,即Key-Value Observing,官方文档中的介绍是 Key-value obse...
    wilsonhan阅读 1,709评论 1 7
  • KVO概述 键值观察Key-Value-Observer就是观察者模式。 观察者模式的定义:一个目标对象管理所有依...
    小希嘻阅读 624评论 0 0
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 3,017评论 0 26
  • 文:盼盼 认真,是对一件事情,一种思想或者一个承诺表现得特别在意,特别负责任。较真,其实就是过...
    盼盼00128阅读 8,982评论 3 1