KVO与KVC详解

221 阅读7分钟

KVO

什么是KVO

KVO全称Key Value Observing,是苹果提供的一套事件通知机制。

允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

KVO原理

在运行时创建一个原类的子类,有固定格式 NSKVONotifying_原类名称,将对象的isa指针指向新创建的类,重写 NSKVONotifying_原类名称 的-class方法,返回原类的Class.

为KVO类增加中间层
  • 生成中间对象 NSKVONotifying_原类名称
  1. 通过object_getClass得到的是生成的中间对象NSKVONotify_BFPerson,而不是BFPerson。

  2. 要想获得该类真实的对象,需要通过class对象方法获取。上面提到了NSKVONotify_BFPerson重写了其class对象方法,返回的是BFPerson。

    image.png

  3. 动态添加的NSKVONotifying_类的类方法通过KVC实现

    image.png

  • 调用监听的属性设置方法

    1. 如setAge:,都会先调用NSKVONotify_BFPerson对应的属性设置方法。
    2. 调用非监听属性设置方法,如test,会通过NSKVONotify_BFPerson的superclass,找到BFPerson类对象,再调用其[BFPerson test]方法 image.png
  • 小提示

    1. 使用object_setClass方法,使得原class指向中间层对象
KVO执行流程

中间类调用[Foundation _NSSetLongLongValueAndNotify]

image.png

触发线程

KVO行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者Run-loop的处理。手动或者自动调用 -didChange... 会触发KVO通知。

  • KVO是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO会保证下列两种情况的发生:

    首先,如果我们调用一个支持KVO的setter方法,如下所示:self.exchangeRate = 2.345

    1. KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。
    2. 其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRate的accessor方法都会返回同样的值。

应用

Demo
  • 注册观察者

    [self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
    
  • 监听回调

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    
    {
    
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
    
    }
    
  • 手动触发

    1. 禁用自动调用

      //针对每个属性,KVO都会生成一个‘+ (BOOL)automaticallyNotifiesObserversOfXXX’方法,返回是否可以自动调用KVO
      
      //假如实现上述方法,我们会发现,此时改变age属性的值,无法触发KVO,还需要实现手动调用才能触发KVO。
      
      + (BOOL)automaticallyNotifiesObserversOfAge
      
      {
      
      return NO;
      
      }
      
    2. 手动调用实现

      - (void)setAge:(NSInteger)age
      
      {
      
      if (_age != age) {
      
      [self willChangeValueForKey:@"age"];
      
      _age = age;
      
      [self didChangeValueForKey:@"age"];
      
      }
      
      }
      
    3. 1&2两步合并写法

      //age不需要自动调用,age属性之外的(含name)自动调用
      
      + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
      
      {
      
      BOOL automatic = NO;
      
      if ([key isEqualToString:@"age"]) {
      
      automatic = NO;
      
      } else {
      
      automatic = [super automaticallyNotifiesObserversForKey:key];
      
      }
      
      return automatic;
      
      }
      
  • 移除观察者

    [self.person1 removeObserver:self forKeyPath:@"age"];
    
最佳实践
keyPath字符串的弊端

在注册Observe时,传入keyPath为字符串类型,keyPath极容易误写。

  • 优化的方案

    [self.person1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:option context:@"age change"];
    

调试

  • 你可以在 lldb 里查看一个被观察对象的所有观察信息。
    (lldb) po [observedObject observationInfo]
    

KVC

什么是KVC

KVC 允许我们用属性的字符串名称来访问属性,字符串在这儿叫做键。

  • 最简单的 KVC 能让我们通过以下的形式访问属性:@property (nonatomic, copy) NSString *name;

    1. 取值:

      NSString *n = [object valueForKey:@"name"]
      
    2. 设定:

      [object setValue:@"Daniel" forKey:@"name"]
      
    3. 值得注意的是这个不仅可以访问作为对象属性,而且也能访问一些标量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架会为我们自动封装它们。举例来说,如果有以下属性:@property (nonatomic) CGFloat height;

      我们可以这样设置它:

      [object setValue:@(20) forKey:@"height"]
      
  • 键路径(Key Path)

    1. KVC 同样允许我们通过关系来访问对象。假设 person 对象有属性 address,address 有属性 city,我们可以这样通过 person 来访问 city:

      [person valueForKeyPath:@"address.city"]
      
  • 集合的操作

    1. 一个常常被忽视的 KVC 特性是它对集合操作的支持。举个例子,我们可以这样来获得一个数组中最大的值:

      NSArray *a = @[@4, @84, @2];
      
      NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
      
    2. 或者说,我们有一个 Transaction 对象的数组,对象有属性 amount 的话,我们可以这样获得最大的 amount:

      NSArray *a = @[transaction1, transaction2, transaction3];
      
      NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
      

变量查找逻辑

KVC操作变量有可能增加耗时

变量get操作执行顺序
  1. 按照get、、is、_顺序查找对象中是否有对应的方法

    • 如果有则调用getter,执行第5步

    • 如果没有找到,跳转到第2步

  2. 查找是否有countOf和objectInAtIndex: 方法(对应于NSArray类定义的原始方法)以及AtIndexes: 方法(对应于NSArray方法objectsAtIndexes:)

    • 如果找到其中的第一个(countOf),再找到其他两个中的至少一个,则创建一个响应所有 NSArray方法的代理集合对象,并返回该对象(即要么是countOf + objectInAtIndex:,要么是countOf + AtIndexes:,要么是countOf + objectInAtIndex: + AtIndexes:)
    • 如果没有找到,跳转到第3步
  3. 查找名为countOf、enumeratorOf和 memberOf这三个方法(对应于NSSet类定义的原始方法)

    • 如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象
    • 如果没有找到,跳转到第4步
  4. 判断accessInstanceVariablesDirectly

    • 为YES时按照_、_is、、is的顺序查找成员变量,找到了就取值
    • 为NO时跳转第6步
  5. 判断取出的属性值

    • 属性值是对象,直接返回
    • 属性值不是对象,但是可以转化为NSNumber类型,则将属性值转化为NSNumber 类型返回
    • 属性值不是对象,也不能转化为NSNumber类型,则将属性值转化为NSValue类型返回
  6. 调用valueForUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

变量set操作执行顺序
  1. 按set:、_set:顺序查找对象中是否有对应的方法

    • 找到了直接调用设值
    • 没有找到跳转第2步
  2. 判断accessInstanceVariablesDirectly结果

    • 为YES时按照_、_is、、is的顺序查找成员变量,找到了就赋值,找不到就跳转第3步
    • 为NO时跳转第3步
  3. 调用setValue:forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

最佳实践

简化列表UI

通过字符串匹配,批量更新属性值

  • 假设我们有这样一个对象:

    
    @interface Contact : NSObject
    
    @property (nonatomic, copy) NSString *name;
    
    @property (nonatomic, copy) NSString *nickname;
    
    @property (nonatomic, copy) NSString *email;
    
    @property (nonatomic, copy) NSString *city;
    
    @end
    
    
  • 还有一个 detail 视图控制器,含有四个对应的 UITextField 属性:

    
    @interface DetailViewController ()
    
    @property (weak, nonatomic) IBOutlet UITextField *nameField;
    
    @property (weak, nonatomic) IBOutlet UITextField *nicknameField;
    
    @property (weak, nonatomic) IBOutlet UITextField *emailField;
    
    @property (weak, nonatomic) IBOutlet UITextField *cityField;
    
    @end
    
    
  • 我们可以简化更新 UI 的逻辑。首先我们需要两个方法:一个返回 model 里我们用到的所有键的方法,一个把键映射到对应的文本框的方法:

    
    - (NSArray *)contactStringKeys;
    
    {
    
    return @[@"name", @"nickname", @"email", @"city"];
    
    }
    
    - (UITextField *)textFieldForModelKey:(NSString *)key;
    
    {
    
    return [self valueForKey:[key stringByAppendingString:@"Field"]];
    
    }
    
    
  • 有了这个,我们可以从 model 里更新文本框,如下所示:

    
    - (void)updateTextFields;
    
    {
    
    for (NSString *key in self.contactStringKeys) {
    
    [self textFieldForModelKey:key].text = [self.contact valueForKey:key];
    
    }
    
    }
    
    
  • 我们也可以用一个 action 方法让四个文本框都能实时更新 model:

    
    //注意:我们之后会添加验证输入的部分,在键值验证里会提到。
    
    - (IBAction)fieldEditingDidEnd:(UITextField *)sender
    
    {
    
    for (NSString *key in self.contactStringKeys) {
    
    UITextField *field = [self textFieldForModelKey:key];
    
    if (field == sender) {
    
    [self.contact setValue:sender.text forKey:key];
    
    break;
    
    }
    
    }
    
    }
    
    
  • 最后,我们需要确认文本框在需要的时候被更新:

    
    - (void)viewWillAppear:(BOOL)animated;
    
    {
    
    [super viewWillAppear:animated];
    
    [self updateTextFields];
    
    }
    
    - (void)setContact:(Contact *)contact
    
    {
    
    _contact = contact;
    
    [self updateTextFields];
    
    }
    
    
修改系统控件属性

系统控件头文件,可控制的变量无法满足要求,寻找更多属性设置

  • 通过runtime获取系统空间的变量列表,以NSImageView为例

    u_int count;
    
    objc_property_t *properties =class_copyPropertyList([NSImageView class], &count);
    
    NSMutableArray *propertiesArray = [NSMutableArray arrayWithCapacity:count];
    
    for (int i = 0; i<count; i++) {
    
    const char* propertyName = property_getName(properties[i]);
    
    MMInfo(@"属性%@\n",[NSString stringWithUTF8String: propertyName]);
    
    [propertiesArray addObject: [NSString stringWithUTF8String: propertyName]];
    
    }
    
  • 得到属性列表,看到placeholderImage字段 image.png

  • 更改私有量的placeholderImage值,实现默认图片设置

    [self setValue:[NSImage imageNamed:@"WeChatLogo"] forKey:@"placeholderImage"];
    

引用

Objective-C(九)KVC与KVO
KVC 和 KVO
Key-Value Coding Programming Guide
iOS探索 KVC原理及自定义