美文网首页iOS基本功iOS面试总结将来跳槽用
iOS键值编码(KVC)与键值监听(KVO)、

iOS键值编码(KVC)与键值监听(KVO)、

作者: 晴天ccc | 来源:发表于2019-03-13 15:47 被阅读70次

    描述

    KVO全称KeyValueObserving。俗称“键值监听”。
    利用Key来找到某个对象并监听其属性的改变。也是一种典型的观察者模式。
    在某个对象注册监听者后,被监听对象的属性发生改变时,会发送一个通知给监听者。以便监听者执行回调操作。

    本文演示代码地址

    KVO方法介绍

    1、通过addObserver:forKeyPath:options:context:方法注册观察者。

    /**
     添加KVO监听
    
     @param observer 添加观察者,被观察者属性变化通知的目标对象
    
     @param keyPath  监听的属性路径
    
     @param options  监听类型 - options支持按位或来监听多个事件类型
    
     @param context  监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
    
     */
    
    - (void)addObserver:(NSObject *)observer
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(nullable void *)context;
    

    2、通过 observeValueForKeyPath:ofObject:change:context:获得回调,从而做出事件处理。

    /**
     监听器对象的监听回调方法
    
     @param keyPath 监听的属性路径
    
     @param object 被观察者
    
     @param change 监听内容的变化
    
     @param context context为监听上下文,由add方法回传
    
     */
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                           context:(void *)context;
    

    3、当观察者不需要监听时,调用可以removeObserver:forKeyPath:方法将KVO移除。需要注意的是:调用removeObserver需要在观察者消失之前,否则会导致Crash。

    - (void)dealloc{
        [self removeObserver:self forKeyPath:@"keyFlag"];
    }
    
    

    简单示例

    @interface ViewController ()
    
    @property (nonatomic,strong) Animal * ani;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.ani = [[Animal alloc] init];
    
        self.ani.age = 10;
        
      // 添加键值监听
        [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
        
    
    }
    
    // 点击事件,触发属性修改
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.ani.age += 5;
    }
    
    // 获得回调,实时监听属性改变、
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
    }
    
    // 需要在不使用的时候,移除监听
    - (void)dealloc{
        [self.ani removeObserver:self forKeyPath:@"age"];
    }
    
    @end
    
    

    KVO原理探究

    1、利用RuntimeAPI动态生成一个子类NSKVONotifying_XXX,并且让当前instance对象isa指针指向这个全新子类。
    2、当修改instance对象的属性时,会触发setter方法,调用Foundation的_NSSetXXXValueAndnotify函数

    • 调用willChangeValueForKey:
    • 调用原来的setter实现(父类原来的setter方法)
    • 调用didChangeValueForKey
      此时内部触发监听器(Oberser)的监听方法 - observeValueForKeyPath: ofObject: change: context:

    代码验证上述流程

    第一步:通过runtime查看isa指针指向的 class对象

    如果观察 Animal的age属性。
    系统会在运行时生成NSKVONotifying_Animal
    在NSKVONotifying_Animal中重写setterclassdealloc等方法。
    使Animal实例对象的isa指针指向NSKVONotifying_Animal
    NSKVONotifying_Animal的superclass指向Animal

    探究过程

        // 注册成为观察者
        NSLog(@"添加KVO之前,Animal的class是 = %s",object_getClassName(self.ani));
        [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
        NSLog(@"添加KVO之后,Animal的class是 = %s",object_getClassName(self.ani));
    
    

    结果如下:

     添加KVO之前,Animal的class是 = Animal
     添加KVO之后,Animal的class是 = NSKVONotifying_Animal
    

    注册成为观察者之后,类变成了NSKVONotifying_Animal而再是 Animal
    我们先看一下NSKVONotifying_Animal类内部的方法。

     #import <objc/runtime.h>
    
    //打印某个类中的所有方法
    - (void)printMethonNamesFromClass:(Class)cls{
        
        unsigned int count;
        //获取方法列表
        Method *methodList = class_copyMethodList(cls, &count);
        
        //保存方法名
        NSMutableString *methonNames = @"".mutableCopy;
        
        for (int i = 0; i < count; i++) {
            
            //获取方法
            Method method = methodList[i];
            
            NSString *methodName = NSStringFromSelector(method_getName(method));
            
            [methonNames appendFormat:@"%@", [NSString stringWithFormat:@"%@, ",methodName]];
            
        }
        
        NSLog(@"methonNames = %@",methonNames);
        //c语音创建的list记得释放
        free(methodList);
    }
    

    结果如下:

     [self printMethonNamesFromClass:object_getClass(self.ani)];
     ----------------------------------------------
     methonNames = setAge:, class, dealloc, _isKVOA,
    
    画图分析KVO内部结构

    第二步:- (void)setAge:(int)age方法

    为了比较在注册观察者前后setter方法的变化,我们新创建一个实例ani1

        self.ani = [[Animal alloc] init];
        self.ani1 = [[Animal alloc] init];
    
        NSLog(@"添加KVO之前,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
              [self.ani methodForSelector:@selector(setAge:)],
              [self.ani1 methodForSelector:@selector(setAge:)]);
    
        // 注册成为观察者
        [self.ani addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
        
        NSLog(@"添加KVO之后,ani的setAge是 = %p,未添加KVO的ani1的setAge是 = %p",
              [self.ani methodForSelector:@selector(setAge:)],
              [self.ani1 methodForSelector:@selector(setAge:)]);
    

    结果如下:

    添加KVO之前,ani的setAge是 = 0x10d751460,未添加KVO的ani1的setAge是 = 0x10d751460
    添加KVO之后,ani的setAge是 = 0x10daaacf2,未添加KVO的ani1的setAge是 = 0x10d751460
    

    这里可以看到,添加KVO前后,setAge方法有所改变
    我们进入debugger来看看这第这个方法的实现到底是怎样的:

    (gdb) print (IMP) 0x10daaacf2 
    $1 = (IMP) 0x96a1a550 <_NSSetIntValueAndNotify> 
    

    原来在重写的NSKVONotifying_Animal-setAge方法中会调用_NSSetIntValueAndNotify:

    // 注:Foundation框架中类似_NSSetIntValueAndNotify的方法实现还有很多:
    __NSSetBoolValueAndNotify
    __NSSetCharValueAndNotify
    __NSSetDoubleValueAndNotify
    __NSSetFloatValueAndNotify
    __NSSetIntValueAndNotify
    __NSSetLongLongValueAndNotify
    __NSSetLongValueAndNotify
    __NSSet0bjectValueAndNotify
    __NSSetPointValueAndNotify
    __NSSetRangeValueAndNotify
    __NSSetRectValueAndNotify
    __NSSetShortValueAndNotify
    __NSSetSizeValueAndNotify
    
    查看_NSSet*ValueAndNotify的内部实现
    - (void)setAge:(int)age{
        _NSSet*ValueAndNotify();
    }
    
    // 因为_NSSetIntValueAndNotify在Foundation框架中,无法查看起具体实现,根据实践猜测大致为代码如下:
    void _NSSet*ValueAndNotify()
    {
          [self willChangeValueForKey:@"age"];
          [super setAge:age];
          [self didChangeValueForKey:@"age"];
    }
    - (void)didChangeValueForKey:(NSString *)key{
              //通过监听器,监听属性发生了改变
      [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
    }
    
    在Animal类中验证
    #import "Animal.h"
    
    @implementation Animal
    
    //Animal内部代码实现
    - (void)setAge:(int)age{
        _age = age;
        NSLog(@"setAge");
    }
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey == begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey == end");
    }
    @end
    
    

    结果如下:

    kvoAndkvoDemo[2391:50226] willChangeValueForKey
    kvoAndkvoDemo[2391:50226] setAge
    kvoAndkvoDemo[2391:50226] didChangeValueForKey == begin
    kvoAndkvoDemo[2391:50226] 监听到了<Animal: 0x600000da3690>对象的age属性由{
        kind = 1;
        new = 15;
        old = 10;
    }变成了{
        kind = 1;
        new = 15;
        old = 10;
    }属性
    kvoAndkvoDemo[2391:50226] didChangeValueForKey == end
    

    如果不添加监听,则不会执行willChangeValueForKeydidChangeValueForKey方法、验证成功!

    汇总

    1、当你观察一个对象时,系统通过Runtime动态的创建一个该类的派生类,这个类继承自该对象的原本的类,并了重写被观察属性的setter方法。
    2、isa指针会指向这个新创建的类,该对象就变成新创建子类的实例了、
    3、重写的setter方法,执行_NSSet*ValueAndNotify,会负责在调用原来的setter方法前后,通知所有观察对象:值的改变。

    拓展思考

    1、用法听起来和NSNotification很相似啊, 其实NSNotification也是观察者模式,但是NSNotification是一种广播机制,KVO是被观察者直接发消息给观察者,是对象间的相互沟通。NSNotification则是两者都和通知中心对象交互,对象之间不知道彼此。
    2、KVO行为是同步的,并且发生与观察的值发生在同样的线程上,没有队列或Run-Loop处理。【使用注意】

    用途

    常见运用是监听ScrollViewcontentOffset属性。当用户滚动结束时动态改变某些空间的实现效果。下拉刷新,渐变导航栏,头像变大缩小等。


    KVC

    KVC (Key-Value-Coding )键值编码。顾名思义:可以通过一个Key来访问某个属性。

    常用方法
    - (void)setValue:(id)value forKey:(NSString *)key;
    - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
    
    - (id)valueForKey:(NSString *)key; 
    - (id)valueForKeyPath:(NSString *)    keyPath;
    
    简单示例
        self.ani = [[Animal alloc] init];
        [self.ani setValue:@38 forKey:@"age"];
        NSLog(@"%d",self.ani.age);
        
        self.ani1 = [[Animal alloc] init];
        [self.ani1 setValue:@99 forKeyPath:@"cat.weight"];
        NSLog(@"%d",self.ani1.cat.weight);
    
        self.ani2 = [[Animal alloc] init];
        self.ani2.age = 10;
        NSLog(@"%@",[self.ani2 valueForKey:@"age"]);
    
    setValue:forKey:的原理:

    当我们设置setValue:forKey:时
    首先会查找setKey:、_setKey: (按顺序查找)
    如果有直接调用
    如果没有,先查看accessInstanceVariablesDirectly方法
    如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
    找到直接复制
    未找到报错NSUnkonwKeyException错误

    valueForKey:的原理:

    kvc取值按照 getKey、key、iskey、_key 顺序查找方法
    存在直接调用
    没找到同样,先查看accessInstanceVariablesDirectly方法
    如果可以访问会按照 _key、_isKey、key、iskey的顺序查找成员变量
    找到直接复制
    未找到报错NSUnkonwKeyException错误

    思考

    我们可以通过 self.ani.age = 10; 来赋值,也可通过上述代码进行赋值,看着多此一举、
    可是如果人这个类的属性是没有暴露在外面呢?比如现在给人这个类一个私有的身高的属性。就可以通过KVC进行赋值、

    Key 和 KeyPath区别接联系

    Key:只能访问当前对象的属性,如果按路径找会报错。
    KeyPath:相当于根据路径去寻找属性,能利用运算符一层一层往内部访问属性。

    用途

    我们通过KVC可以直接对私有属性并进行赋值
    字典转模型

    拓展

    我们通过XIB或者SB拖线布局连线错误的时候也会报错说找不到什么key,说明Storyboard在赋值的时候也是通过KVC来操作的。

    试题

    KVO相关:
    1. iOS用什么方式来实现对一个对象的KVO?(KVO的本质是什么?)
    2. 如何手动出发KVO?
    3. 直接修改成员变量会触发KVO么?
    
    KVC相关: 
    1. 通过KVC修改属性会触发KVO么?
    2. KVC的赋值和取值过程是怎样的?原理是什么?
    

    相关文章

      网友评论

        本文标题:iOS键值编码(KVC)与键值监听(KVO)、

        本文链接:https://www.haomeiwen.com/subject/fogzpqtx.html