KVO的底层原理和自实现KVO

一、KVO的概述

KVO : Key-Value Observing(观察者设计模式)

当被观察对象的值改变时,观察者就会受到通知,这种模式有利于两个类间的解耦合,尤其是对于业务逻辑与视图控制 这两个功能的解耦合。

二、KVO的实现原理

  • 1、KVO是基于runtime机制实现的;
  • 2、当某个类(Person)的属性被观察时,系统会自动的派生一个该类的子类(NSKVONotifying_Person)
  • 3、为该子类添加setter方法;
  • 4、消息转发,子类(NSKVONotifying_Person)--> 父类(Person);
  • 备注:苹果粑粑还悄悄的重写了该类的class方法,从而让我们感知不到子类的存在;

三、自实现KVO(观察者模式)

首先看下系统的KVO

//给self.Person属性添加观察者   
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

//通知观察者
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"通知了观察者,被观察的值改变了");
    }
}

下面我们自己实现KVO
话不多说,代码撸起 Demo

//NSObject+CGQKVO.h

#import  <Foundation/Foundation.h>
#import <objc/message.h>

typedef void (^QZKVOBlock)(id observer, id keyPath, id newValue, id oldValue);
@interface NSObject (CGQKVO)

/**
添加观察者
@param observer 观察者对象
@param keyPath 需要观察的键值
@param handle 回调block
*/
- (void)qz_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath withHandle:(QZKVOBlock)handle;

/**
移除观察者
@param observer 观察值对象
@param keyPath 观察的键值
*/
-(void)qz_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end

//创建一个info类来保存观察的数据
@interface QZ_Info : NSObject
@property (nonatomic, weak) id observer;
@property (nonatomic,copy) NSString *keyPath;
@property (nonatomic,copy) QZKVOBlock handle;
-(instancetype)initWhit:(id)observer keyPath:(NSString *)keyPath handle:(QZKVOBlock)handle;
@end

@implementation QZ_Info
-(instancetype)initWhit:(id)observer keyPath:(NSString *)keyPath handle:(QZKVOBlock)handle{
    self = [super init];
    if (self) {
        _observer = observer;
        _keyPath = keyPath;
        _handle = handle;
    }
    return self;
}

@end
//NSObject+CGQKVO.m

#import "NSObject+CGQKVO.h"
static NSString *const QZKVOPrefix = @"QZKVO_";
static NSString *const QZKVOAssicationKey = @"QZKVOAssicationKey";

@implementation NSObject (CGQKVO)
- (void)qz_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath withHandle:(QZKVOBlock)handle{

    //检查是否是实例变量(实例变量就抛出异常)
    //1.获取当前类
    Class superClass = object_getClass(self);
    //2.检查是否有setter方法
    SEL setterSeletor = NSSelectorFromString(setterFormGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);

    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"这个key:%@,没有setter:方法",keyPath] userInfo:nil];
    }

    //动态创建子类创建子类
    NSString *superClassName = NSStringFromClass(superClass);
    Class childClass;
    if (![superClassName hasPrefix:QZKVOPrefix]) {
        childClass = [self createClassFromSuperClass:superClassName];
        //把动态创建的子类指向父类
        //isa_swizzling-->黑魔法
        object_setClass(self, childClass);
    }

    //为子类添加setter方法
    /**
    class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
    1.class -->  给谁添加
    2.SEL  -->  方法编号
    3.IMP  -->  函数指针,指向函数实现
    4.types -->  返回值,参数
    */
    const char *types = method_getTypeEncoding(setterMethod);
    class_addMethod(childClass, setterSeletor, (IMP)QZKVO_Setter, types);
    
    //添加回调,方便调用者处理
    QZ_Info *info = [[QZ_Info alloc] initWhit:observer keyPath:keyPath handle:handle];
    NSMutableArray *Arr = objc_getAssociatedObject(self, &QZKVOAssicationKey);
    if (!Arr) {
        Arr = [[NSMutableArray alloc] initWithCapacity:1];
        objc_setAssociatedObject(self, &QZKVOAssicationKey, Arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [Arr addObject:info];
}

-(void)qz_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    NSMutableArray *Arr = objc_getAssociatedObject(self, &QZKVOAssicationKey);
    QZ_Info *tmp;
    for (QZ_Info *info in Arr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            tmp = info;
            break;
        }
    }
    if (tmp) {
        [Arr removeObject:tmp];
    }
}

/**
通过父类创建子类
@param superClassName 父类类名
@return 子类
*/
- (Class)createClassFromSuperClass:(NSString *)superClassName{

    //获取父类
    Class superClass = object_getClass(self);

    //创建新类
    //1.获取子类类名
    NSString *childClassName = [QZKVOPrefix stringByAppendingString:NSStringFromClass(superClass)];

    //2.检查是否创建过该子类
    Class childClass = NSClassFromString(childClassName);
    //创建过就直接返回改类
    if (childClass) return childClass;

    //没创建过就开始创建
    /**
    1.superClass --> 父类
    2.childClassName --> 创建的类名
    3.size_t extraBytes -->开辟的内存空间
    */
    childClass = objc_allocateClassPair(superClass, childClassName.UTF8String, 0);

    /**
    添加子类动态添加的class方法
    class_addMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
    1.class --> 给谁添加
    2.SEL  --> 方法编号
    3.IMP  --> 函数指针,指向函数的实现
    4.types --> 返回值/参数
    */
    Method childClassMethod = class_getInstanceMethod(superClass, @selector(class));//获取父类的class方法
    const char *types = method_getTypeEncoding(childClassMethod);
    class_addMethod(childClass, @selector(class),(IMP)QZKVO_Class, types);

    //注册
    objc_registerClassPair(childClass);

    return childClass;
}

#pragma mark - 函数区域

static void QZKVO_Setter(id self, SEL _cmd, id newValue){
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterFormSetter(setterName);
    if (!getterName) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"%@没有getter:%@方法",self,getterName] userInfo:nil];
    }

    //取出旧值
    id oldValue = [self valueForKey:getterName];

    //即将改变值
    [self willChangeValueForKey:getterName];

    //消息转发 子类-->父类
    /**
    objc_msgSendSuper(void * struct objc_super *super, SEL op, ... *)
    1.super --> void * struct objc_super 结构体
    2.op    --> SEL方法编号
    */
    void (*qzkvo_megSendSuper)(void *, SEL, id) = (void*)objc_msgSendSuper;

    struct objc_super qz_objcSuper = {
        /// Specifies an instance of a class.
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };

    qzkvo_megSendSuper(&qz_objcSuper, _cmd, newValue);

    //已经改变值
    [self didChangeValueForKey:getterName];

    NSArray *Arr = objc_getAssociatedObject(self, &QZKVOAssicationKey);
    for (QZ_Info *info in Arr) {
        if ([info.keyPath isEqualToString:getterName]) {
            info.handle(info.observer, info.keyPath, newValue, oldValue);
        }
    }
}

static Class QZKVO_Class(id self){
    return class_getSuperclass(object_getClass(self));
}

//name --> setName:
static NSString *setterFormGetter(NSString * getter){

    if (getter.length == 0)   return nil;

    NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *lastString = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",firstString,lastString];
}

//setName: --> name
static NSString *getterFormSetter(NSString * setter){

    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
        return nil;
    }
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getterStr = [setter substringWithRange:range];
    NSString *firstString = [[getterStr substringToIndex:1] lowercaseString];
    return [getterStr stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

@end

总结

KVO实际上就是观察对象的setter方法,对于没有setter方法对象和不调用setter方法(_name = @"张三")时,值的改变,是无法触发KVO机制的;
KVO机制能够很方便的提供两个对象间的同步(像view和model之间的同步)
自己完成实现KVO后,会对KVO底层有一个较深入的了解,增加了block回调也能增加代码的可读性和可维护性;

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

推荐阅读更多精彩内容