KVO & KVC

作者: 曹来东 | 来源:发表于2018-08-27 16:59 被阅读16次

    KVO:

    KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变.

        self.person = [[LDPerson alloc] init];
        self.person.age = 10;
    //添加键值监听
        [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    //在touchesBegan方法中修改age属性值. 
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.person.age = 20;
    //就会在下面的方法中监听到变化的具体信息,哪个对象的哪个属性,变化前后的属性值.
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
    }
    //NSLog信息:
    监听到了<LDPerson: 0x60c00000fab0>对象的age属性由{
        kind = 1;
        new = 20;
        old = 10;
    }变成了{
        kind = 1;
        new = 20;
        old = 10;
    }属性
    //需要在不使用的时候,移除监听
    - (void)dealloc{
        [self.person removeObserver:self forKeyPath:@"age"];
    }
    

    KVO原理:

    通过修改instance对象的isa指针,指向一个通过runtime动态生成的子类,例如self.personisa指针并不是指向LDPersonclass对象,而是指向NSKVONotifying_LDPerson,该类是LDPerson的一个子类.
    未使用KVO监听的对象:

    image.png
    使用了KVO监听的对象:
    image.png

    _NSSet*ValueAndNotify的内部实现

    - (void)setAge:(int)age{
        _NSSet*ValueAndNotify();
    }
    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];
    }
    
    image.png

    1.调用willChangeValueForKey:
    2.调用原来的setter实现
    3.调用didChangeValueForKey:

    • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

    代码验证上述流程

    //LDPerson内部代码实现
    - (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");
    }
    

    通过打印信息可以看到,流程如上所述:


    image.png

    通过runtime查看isa指针指向的 class对象

    可以看到添加KVO之后,instacne对象的isa所指向的class对象发生了变化.

    image.png

    通过LLDB查看- (void)setAge:(int)age方法.

    • 如果LDPerson实现了setAge会提示(KVO-[LDPerson setAge:] at LDPerson.m:12)setAge在.m文件当中第12行.
    • 如果LDPerson只是通过@property则会提示在.h文件中.
    • 可以观察到添加了KVO属性的setter实现不同.添加之前是在LDPerson文件中,添加KVO之后setter实现是在Foundation框架中,_NSSet*ValueAndNotify
    • runtime动态生成的类NSKVONotifying_LDPerson,其isa指向该类的元类对象NSKVONotifying_LDPerson,其superClass指针指向LDPerson,即生成的class对象有自己的meta-class对象.其父类为添加KVO对象的class对象LDPerson`.
      image.png

    动态生成的NSKVONotifying_LDPerson类

    runtime动态生成的这个类内部有几个特殊的方法,并重写了setAge,class,dealloc方法

    - (void)setAge:(int)age
    {
        _NSSetIntValueAndNotify();
    }
    
    // 屏幕内部实现,隐藏了NSKVONotifying_LDPerson类的存在
    - (Class)class
    {
        return [LDPerson class];
    }
    
    - (void)dealloc
    {
        // 收尾工作
    }
    
    - (BOOL)_isKVOA
    {
        return YES;
    }
    
    

    为何重写class方法:
    屏幕内部实现,隐藏了NSKVONotifying_LDPerson类的存在.从开发者的使用角度来看,并不知道NSKVONotifying_LDPerson类的存在,如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_LDPerson,就会将该类暴露出来,也给开发者造成困扰,写的是LDPerson,添加KVO之后class方法返回会改变.
    猜测NSObjectclass方法的实现:
    self为添加了KVO后的LDPerson,最红返回可能是NSKVONotifying_LDPerson.

    - (Class)class
    {
        return object_getClass(self);
    }
    

    通过runtime 检验class,dealloc``isKVOA方法的存在

    - (void)printMethodNameOfClass:(Class)cls{
        unsigned int count;
        //获取方法列表数组
        Method * methodList = class_copyMethodList(cls, &count);
        
        //存储方法名
        NSMutableString * methodsNames = [NSMutableString string];
        
        //遍历方法数组
        for (int i = 0; i < count; i++) {
            //获取方法
            Method  method = methodList[I];
            //获取方法名字
            NSString * methodName = NSStringFromSelector(method_getName(method));
            //拼接方法
            [methodsNames appendString:methodName];
            [methodsNames appendString:@", "];
        }
        //释放 如果C语言的数据结构是是通过copy
        //creat 等创建出来的,一般都需要释放
        free(methodList);
        
        //打印方法名
        NSLog(@"%@ %@",cls,methodsNames);
    }
    self.person1 = [[LDPerson alloc] init];
        self.person1.age = 10;
      
        self.person2 = [[LDPerson alloc] init];
        self.person2.age = 20;
        
        [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
        //person1 添加了KVO
        [self printMethodNameOfClass:object_getClass(self.person1)];
        //person2 没有添加了KVO
        [self printMethodNameOfClass:object_getClass(self.person2)];
    

    可以看到:

    • 添加了KVO的NSKVONotifying_LDPerson类的内部有class,dealloc``isKVOAsetter方法.没有agegetter方法.因为NSKVONotifying_LDPerson类只会重写setter方法,该setter方法内部会调用willChangeValueForKey,didChangeValueForKey来实现KVO监听.而getter方法存在NSKVONotifying_LDPerson类的父类LDPerson中.
    • 没有添加KVO的LDPerson只有一个property属性,还属性会生成settergetter方法.
      image.png

    iOS用什么方式实现对一个对象的KVO?(KVO本质是什么?)

    • 利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的子类.
    • 当修改instance对象的属性时,会调用Foundation_NSSetxxxValueAndNotify函数该函数会调用以下函数:
      1. willChangeValueForKey
      2. 父类原来的setter方法
      3. didChangeValueForKey该函数内部会出发监听器(Oberser)的监听方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

    如何手动触发KVO

    解读:添加KVO之后正常情况下触发KVO的条件是:修改了KVO监听的属性值,此时会触发KVO监听方法.此题的意思是在没有修改KVO监听的属性值时,如果触发KVO监听方法?
    思路:动态生成的子类NSKVONotifying_LDPerson内部主要的操作是重写了setter方法,方法内部调用了
    willChangeValueForKey,didChangeValueForKey方法.
    所以:
    我们只需要被监听的 instance对象手动调用两个方法即可:

      [self.person1 willChangeValueForKey:@"age"];
      [self.person1 didChangeValueForKey:@"age"];
    

    哪些对象可以使用KVO监听?核心是重写了setter,如果有setter就可以实现监听,如果没有setter就无法使用KVO监听.

    直接修改成员变量会触发KVO吗

    不会,没有在setter中调用willChangeValueForKey, didChangeValueForKey核心方法.

    KVC

    KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性

    常见的API有:

    • -(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
    • - (void)setValue:(id)value forKey:(NSString *)key;
    • - (id)valueForKeyPath:(NSString *)keyPath;
    • - (id)valueForKey:(NSString *)key;

    setValue:(id)value forKey和setValue:(id)value forKeyPath有什么不同?

    value forKeyPath会通过路径向下级继续查找并赋值
    value forKey只会在当前对象的路径下赋值,不会继续查找

    @interface LDCat : NSObject
    @property (assign, nonatomic) int weight;
    @end
    @interface ldPerson : NSObject
    @property (assign, nonatomic) int age;
    @property (assign, nonatomic) LDCat *cat;
    @end
    
     [person valueForKey:@"age"];
     [person setValue:@10 forKeyPath:@"cat.weight"];
    

    通过KVC修改属性会触发KVO吗?(会)

    解读:该题的意思是如下代码会不会出发KVO.

    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person1 = [[LDPerson alloc] init];
        self.person1.age = 10;
        [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [self.person1 setValue:@20 forKey:@"age"];
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"监听到了%@对象的%@属性改变%@",object,keyPath,change);
    }
    - (void)dealloc{
        [self.person1 removeObserver:self forKeyPath:@"age"];
    }
    KVC的核心是在`setter`中调用`willChangeValueForKey, didChangeValueForKey`方法,上图代码中不用`@property`关键字生成`setter方法,可以看到依然会触发KVO,可知KVC内部会自动调用上述`willChangeValueForKey, didChangeValueForKey`方法来触发KVO监听
    

    KVC的赋值和取值过程是怎样的,原理是什么?

    赋值

    image.png

    解读:如果调用了setValue: forKey:方法
    1.会首先查找setKey:方法,如果没有setKey:方法,接下来会查找_setKey:方法.至此如果查找到两个方法中的热议一个,就会传递参数,调用方法,出发KVO.
    2.如果上述两个方法都没有查找到.会查看accessInstanceVariablesDirectly的返回值.该方法标识是都可以直接访问成员变量,如果返会NO即不能直接访问成员变量,就会抛出异常,找不到合适的Key赋值.如果可以直接访问成员变量,接下来就会查找属性名,按照如下顺序查找:_key _isKey key isKey如果找到成员变量直接赋值,触发KVO.

    KVC过程总结:

    1. 首先按照顺序查找setter方法
      1.1: setKey: --->_setKey:

    2:如果没有查找到setter方法,会查看accessInstanceVariablesDirectly(是否可以直接访问成员变量,默认返回YES)函数返回值.如果返回值为NO就会调用setValue:forUndefinedKey:
    并抛出异常NSUnknownKeyException.如果返回值为YES,

    3: 接下来会进入查找成员变量,直接赋值的流程
    查找顺序为:_Key --> _isKey ---> key --->isKey

    4.如果没有查找到成员变量,就会调用setValue:forUndefinedKey:
    并抛出异常NSUnknownKeyException

    valueForKey:取值过程

    image.png
    1.查找getter方法
    getKey ---> Key ---> isKey---> _key
    找到该方法,调用并返回对应的值,如果没找到
    2.查看accessInstanceVariablesDirectly方法返回值
    如果返回YES进入下个流程,如果返回NO就会调用setValue:forUndefinedKey:
    并抛出异常NSUnknownKeyException
    3.返回值为YES即可直接访问成员变量,就会俺书讯查找成员变量: _key ---> _iskey ---> key ---> iskey
    4.如果查找到调用并返回对应的值.如果没找到就会调用setValue:forUndefinedKey:
    并抛出异常NSUnknownKeyException

    相关文章

      网友评论

          本文标题:KVO & KVC

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