• KVC && KVO


    一、什么是KVC?

    KVC(Key-value coding)键值编码,它提供了一种通过key间接访问对象的属性或成员变量的方法,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性或成员变量,而不是在编译时确定。

    KVC的使用环境:

    无论是property还是普通的全局属性变量,都可以使用KVC;

    KVC优点:

    • 主要的好处就是减少代码量;
    • 没有property的变量(即:私有变量private)也能通过KVC进行设置。

    KVC缺点:

    如果key只写错,编写的时候不会报错,但是运行的时候会报错;

    KVC使用的基本方法:

     //默认返回YES,表示如果没有找到Set<Key>方法的话,
     //会按照_key,_iskey,key,iskey的顺序搜索成员,
     //设置成NO就不这样搜索
     + (BOOL)accessInstanceVariablesDirectly;
     
     //KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、
     //为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
     - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
     
     //这是集合操作的API,里面还有一系列这样的API,
     //如果属性是一个NSMutableArray,那么可以用这个方法来返回。
     - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
     
     //如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,
     //则会调用这个方法,默认是抛出异常。
     - (nullable id)valueForUndefinedKey:(NSString *)key;
     
      //和上一个方法一样,但这个方法是设值。
     - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
    
     //如果你在SetValue方法时面给Value传nil,则会调用这个方法
     - (void)setNilValueForKey:(NSString *)key;
     
     //输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
     - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

    二、KVC设值

    假设在一个 TestOjb 类中,有一个成员变量 name 表示姓名,正常情况下,在外界是无法设置或获取其值的。那么用KVC是如何实现的呢?KVC要设值,是根据对象中的对应的 key 来决定的。KVC在内部又是按什么样的顺序来寻找 key 的呢?

    当调用 setValue:@" " forKey:@" " 方法时,底层的执行机制如下:

    1、首先搜索是否有 setKey:的方法(Key是成员变量名), 如果没有则会继续依次搜索是否有_setKey: 、 setIsKey:的方法(首先搜索setter方法);

    2、如果上述方法仍没有找到,此时会调用 + (BOOL)accessInstanceVariablesDirectly 方法(是否直接访问成员变量方法)。

    该方法默认返回YES,会按照_key,_iskey,key,iskey的顺序搜索成员变量名。

    若返回NO,则直接调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key 方法(默认是抛出异常)。

    #import "TestObj.h"
    
    @interface TestObj()
    {
        NSString *_name;  //优先级1
        NSString *_isName; //优先级2
        NSString *name; //优先级3
        NSString *isName; //优先级4
        
    }
    @end
    
    @implementation TestObj
    
    
    
    - (void)setName:(NSString *)name{//优先级1
        NSLog(@"setName");
    }
    - (void)_setName:(NSString *)name{//优先级2
        NSLog(@"_setName");
    }
    - (void)setIsName:(NSString *)name{//优先级3
        NSLog(@"setIsName");
    }
    
    - (id)valueForKey:(NSString *)key{
        NSLog(@"_name:%@", _name);
        NSLog(@"_isName:%@", _isName);
        NSLog(@"name:%@", name);
        NSLog(@"isName:%@", isName);
        return nil;
    }
    
    + (BOOL)accessInstanceVariablesDirectly{
        return YES;
    }
    
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key{
        NSLog(@"setValue异常");
    }
    
    - (id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"valueForUndefinedKey异常");
        return nil;
    }
    
    
    @end
        TestObj *obj = [[TestObj alloc]init];
        [obj setValue:@"小明" forKey:@"name"];
        NSString *name = [obj valueForKey:@"name"];

    通过对代码的注释打印可得优先级正好对应上述搜索机制。

    三、KVC取值

    当调用 valueForKey:@" " 方法时,底层的执行机制如下:

    1、按先后顺序搜索 getKey:、key、isKey(getter方法)三个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

    2、如果上面的getter方法没有找到,则会调用+ (BOOL)accessInstanceVariablesDirectly方法判断是否允许取成员变量的值。

    若返回NO,直接调用- (nullable id)valueForUndefinedKey:(NSString *)key方法,默认是奔溃。 

    若返回YES,会按先后顺序取_key、_isKey、 key、isKey的值。

    3、返回YES时,_key、_isKey、 key、isKey 的值都没取到,调用 - (nullable id)valueForUndefinedKey:(NSString *)key 方法。

     四、什么是KVO?

    KVO 即 Key-Value Observing,键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。也就是说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。

    KVO优点:

    • 能够提供一种简单的方法实现两个对象的同步;
    • 能够对内部对象的状态改变作出响应,而且不需要改变内部对象的实现;
    • 能够提供被观察者属性的最新值和之前的值;
    • 使用key Path来观察属性,因此可以观察嵌套对象;
    • 完成了对观察对象的抽象,因为不需要额外的代码来允许观察者被观察。

    KVO缺点:

    • 我们观察的属性必须使用strings定义,编译时不会出现警告;
    • 对属性重构,将导致观察代码不可用;
    • 复杂的 “if” 语句要求对象正在观察多个值,是因为所有的观察代码通过一个方法来指向;
    • 当释放观察者的时候不需要移除观察者。

    观察者对象与被观察者对象注册与解除注册监听方法

    observer:观察者,也就是KVO通知的订阅者。订阅着必须实现 
    keyPath:描述将要观察的属性,相对于被观察者。
    options:KVO的一些属性配置;有四个选项。
    context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
    
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    
    options所包括的内容
    NSKeyValueObservingOptionNew:change字典包括改变后的值
    NSKeyValueObservingOptionOld:change字典包括改变前的值
    NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
    NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

    处理变更通知

    每当监听的keyPath发生变化了,就会在这个函数中回调。

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context

    在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。

    系统实现KVO有以下几个步骤:

    • 当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
    • 在派生类B中重写类A的setter方法,B类在被重写的setter方法中实现通知机制。
    • 类B重写会 class方法,将自己伪装成类A。类B还会重写dealloc方法释放资源。
    • 系统将所有指向类A对象的isa指针指向类B的对象。
    #pragma mark - KVO
    - (void)TestForKVO{
        self.objs = [[TestObj alloc]init];
        self.objs.phone = @"1234";
        //此行注册监听后,objs由TestObj类变成NSKVONotyfing_TestObj类。
        [self.objs addObserver:self forKeyPath:@"phone" options:NSKeyValueObservingOptionNew context:nil];
       
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"%@监听到%@属性的改变为%@", object, keyPath, change[@"new"]);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.objs.phone = @"kvo实现啦";
    }
    
    - (void)dealloc{
        [self.objs removeObserver:self forKeyPath:@"phone"];
    }

    手动实现键值观察

    @interface TestObj : NSObject
    
    - (void)setAge:(int)age;
    - (int)age;
    
    
    @end
    #pragma mark - KVO
    - (id)init{
        self = [super init];
        if (self) {
            _age = 20;
        }
        return self;
    }
    
    - (int)age{
        return _age;
    }
    - (void)setAge:(int)age{
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }
    
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"age"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }

    首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
    其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

     

    • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
    • 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
    • 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

    KVO与Notification之间的区别:

    notification是需要一个发送notification的对象,一般是notificationCenter,来通知观察者。

    KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。

     
     

    参考:https://www.jianshu.com/p/9183365170bd

       https://www.jianshu.com/p/b9f020a8b4c9

         https://www.cnblogs.com/junhuawang/p/5802325.html

    GitHub:https://github.com/hongsheng1024/RunTime

    
    
    
  • 相关阅读:
    Python导入运行的当前模块报错
    关于相对性的思考
    B+树
    DNS:从零搭建公司内网DNS服务器
    【05】Saltstack:配置详解
    【04】Saltstack:配置管理
    【03】Saltstack:远程执行
    【02】Saltstack:Grains and Pillar
    【01】Saltstack:从零开始 Saltstack
    【08】Kubernets:Service
  • 原文地址:https://www.cnblogs.com/whongs/p/9859215.html
Copyright © 2020-2023  润新知