iOS底层-KVO、KVC

KVO

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

     #import "ViewController.h"
     #import "Person.h"
     #import <objc/runtime.h>
     @interface ViewController ()
     @property (nonatomic,strong) Person *person1;
     @property (nonatomic,strong) Person *person2;
     @end
        
     @implementation ViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.person1 = [[Person alloc] init];
         self.person1.age = 10;
            
         self.person2 = [[Person alloc] init];
         self.person2.age = 20;
         //监听isa的变化
         NSLog(@"===添加KVO之前===%@===%@",object_getClass(self.person1),object_getClass(self.person2));
         //监听setAge的实现变化
         NSLog(@"person1添加KVO监听之前 - %p === person2===%p",
                   [self.person1 methodForSelector:@selector(setAge:)],
                   [self.person2 methodForSelector:@selector(setAge:)]);
         //给person对象添加监听KVO
         NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
         [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"age"];
            
         //监听isa的变化
         NSLog(@"===添加KVO之后===%@===%@",object_getClass(self.person1),object_getClass(self.person2));
            
         //监听setAge的实现变化
         NSLog(@"person1添加KVO监听之后 - %p === person2=== %p",
               [self.person1 methodForSelector:@selector(setAge:)],
               [self.person2 methodForSelector:@selector(setAge:)]);
            
         //监听类实现了那些方法
         [self printMethodNamesOfClass:object_getClass(self.person1)];
         [self printMethodNamesOfClass:object_getClass(self.person2)];
         //打断点处
         NSLog(@"==============");
     }
     - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         self.person1.age = 20;
     }
     //当监听对象的属性值发生改变的时候
        
     - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
            
       NSLog(@"age=== %@====%@===%@",keyPath,object,change);
     }
        
     //打印一个类的所有方法实现
     - (void)printMethodNamesOfClass:(Class)cls
     {
         unsigned int count;
         // 获得方法数组
         Method *methodList = class_copyMethodList(cls, &count);
            
         // 存储方法名
         NSMutableString *methodNames = [NSMutableString string];
            
         // 遍历所有的方法
         for (int i = 0; i < count; i++) {
             // 获得方法
             Method method = methodList[i];
             // 获得方法名
             NSString *methodName = NSStringFromSelector(method_getName(method));
             // 拼接方法名
             [methodNames appendString:methodName];
             [methodNames appendString:@", "];
         }
            
         // 释放
         free(methodList);
            
         // 打印方法名
         NSLog(@"类名===%@== 类中的方法列表=== %@", cls, methodNames);
     }
     -(void)dealloc{
            
         [self removeObserver:self forKeyPath:@"age"];
     }
     @end
    
  3. 打印调试如下:

     //isa指向监听
     ===添加KVO之前===Person===Person
     //setAge方法实现监听
     person1添加KVO监听之前 - 0x10fb5d400 === person2===0x10fb5d400
         
     -------person1调用了addObserver方法
         
     //isa指向监听
     ===添加KVO之后===NSKVONotifying_Person===Person
     //setAge方法实现监听
     person1添加KVO监听之后 - 0x10feb83d2 === person2=== 0x10fb5d400
         
     //当前类的方法列表监听
     类名===NSKVONotifying_Person== 类中的方法列表=== setAge:, class, dealloc, _isKVOA,
     类名===Person== 类中的方法列表=== height, setHeight:, setAge:, age,
         
     //使用lldb命令,查看地址对应的实现方法名
     (lldb) p (IMP)0x10feb83d2
     (IMP) $0 = 0x000000010feb83d2 (Foundation`_NSSetIntValueAndNotify)
     (lldb) po (IMP) 0x10fb5d400
     (-[Person setAge:] at Person.h:15)
    
    1. 可以发现person1执行了addoObserver之后,person1的isa指针指向的不是Person了,而是NSKVONotifying_Person
    2. 说明,person1调用setAge对应的方法实现是_NSSetIntValueAndNotify这个函数,而不是调用setAge方法了
    3. NSKVONotifying_Person这个类内部实现了setAge:, class, dealloc, _isKVOA这4个方法
  4. _NSSet*ValueAndNotify的内部实现 (*表示int、char、double等 )
    1. 从上面Foundation '_NSSetIntValueAndNotify可以看出,_NSSetIntValueAndNotify这个函数的实现在Foundation框架中
    2. 通过手机越狱,导出Foundation框架动态库,然后将动态库文件拖入到Hopper逆向工具,找到该函数,分析该函数内部流程如下:
      1. 调用willChangeValueForKey:
      2. 调用原来的setter实现
      3. 调用didChangeValueForKey:
        1. didChangeValueForKey:内部会调用observerobserveValueForKeyPath:ofObject:change:context:方法
  5. Person对象调用addObserver之后如下:

    图1

相关面试题

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
    1. 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
    2. 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
      1. willChangeValueForKey:
      2. 父类原来的setter
      3. didChangeValueForKey:
        1. 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:
  2. 如何手动触发KVO?
    1. 手动调用willChangeValueForKey:和didChangeValueForKey:
  3. 直接修改成员变量会触发KVO么?
    1. 不会触发KVO,因为本质是监听setter方法

KVC

  1. 代码示例:

     #import <Foundation/Foundation.h>
     @interface Person : NSObject
     {
     @public
     //    int  _age;
     //    int _isAge;
     //    int age;
         int isAge;
     }
     @end
        
     #import "Person.h"
     @implementation Person
     /**
      valueForKey会按照下面顺序寻找
      */
     //-(int)getAge{
     //    return 10;
     //}
     //-(int)age{
     //    return 20;
     //}
     //-(int)isAge{
     //    return 30;
     //}
     //-(int)_age{
     //    return 40;
     //}
        
     /**
      setValue: forKey: 会按照下面顺序寻找
      */
     //-(void)setAge:(int)age{
     //    NSLog(@"setAge=====");
     //}
     //-(void)_setAge:(int)age{
     //    NSLog(@"_setAge=====");
     //}
        
     /*
      是否允许直接返回成员变量
      如果不主动实现,默认返回值就是YES
      如果返回值为YES,那么久允许直接访问成员变量,反之不能
      */
     +(BOOL)accessInstanceVariablesDirectly{
     //    return NO;
         return YES;
     }
        
     /*********证明是否手动触发了KVO**********/
        
     //KVO成员变量修改之前调用
     -(void)willChangeValueForKey:(NSString *)key{
         [super willChangeValueForKey:key];
         NSLog(@"willChangeValueForKey - %@", key);
     }
     //KVO成员变量修改之后调用
     -(void)didChangeValueForKey:(NSString *)key{
         NSLog(@"didChangeValueForKey - begin - %@", key);
         //这个内部会调用observer的observeValueForKeyPath方法
         [super didChangeValueForKey:key];
         NSLog(@"didChangeValueForKey - end - %@", key);
     }
               
     @end
    
     #import "ViewController.h"
     #import "Person.h"
     @interface ViewController ()
     @property (nonatomic,strong) Person *person;
     @end
        
     @implementation ViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         Person *person = [[Person alloc] init];
         self.person = person;
         //KVO监听
         [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
         //KVC赋值
         [person setValue:@(1) forKey:@"age"];
            
         //打印值
         //        NSLog(@"=====%d",person->_age);
         //         NSLog(@"=====%d",person->_isAge);
         //         NSLog(@"=====%d",person->age);
         NSLog(@"=====%d",person->isAge);
            
            
           
     }
        
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         //获取值
         NSLog(@"==valueForKey===%@",[self.person valueForKey:@"age"]);
     }
    
     /**
      1. 发现问题如下:
         1. 即使Person没有实现setAge:方法或者没有age属性,仅仅有_age/_isAge
     /age/isAge成员变量,当KVC赋值的时候,也会触发KVO。
         2. KVO监听的是对象的“age”成员,但是对象即使没有age成员,或者_age成员,只有_isAge或者isAge,也会触发KVO
         3. 说明KVC(setValue:forKey:)内部主动通知了KVO
              //猜测内部实现
              // [person willChangeValueForKey:@"age"];
              // person->_age = 10;
              // [person didChangeValueForKey:@"age"];
         4. 因此,使用KVC修改成员变量值,不管对象内部是否实现setter方法,都会触发KVO
      */
     //当监听对象的属性值发生改变的时候
     - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
            
        NSLog(@"observeValueForKeyPath - %@", change);
     }
        
     - (void)dealloc{
         [self.person removeObserver:self forKeyPath:@"age"];
     }
     @end
    
  2. 经过上面的代码分析,KVC的原理如下图

    1. setValue:forKey:的原理
      1. 到对象寻找是否有setAge方法,找到了传递参数,调用方法,如果没有
      2. 寻找_setAge方法,找到了传递参数,调用方法,如果没有
      3. 获取accessInstanceVariablesDirectly这个类方法的返回值
        1. 返回值为NO,调用setValue:forUndefinedKey:,并抛出异常NSUnknownKeyException
        2. 返回值为YES,继续寻找
      4. 按照_age、_isAge、age、isage顺序查找成员变量,只要找到,直接赋值,没找到
      5. 调用setValue:forUndefinedKey:,并抛出异常NSUnknownKeyException

        图1

    2. valueForKey:的原理
      1. 按照getAge、age、 isAge、_age顺序查找方法,找到直接调用方法,没找到
      2. 获取accessInstanceVariablesDirectly这个类方法的返回值
        1. 返回值为NO,调用valueForUndefinedKey:,并抛出异常NSUnknownKeyException
        2. 返回值为YES,继续寻找
      3. 按照_age、_isAge、age、isAge顺序查找成员变量,找到直接赋值,没找到
      4. 调用valueForUndefinedKey:,并抛出异常NSUnknownKeyException

        图1

面试题

  1. 通过KVC修改属性会触发KVO么?
    1. 会触发KVO (上面分析的有)
  2. KVC的赋值和取值过程是怎样的?原理是什么?
Table of Contents