iOS中的KVO机制

目录

前言

1.KVO的概念和基础用法

1.概念

2.基础用法

2.KVO的本质

1.KVO 的工作原理

2.示例代码解释 KVO 的本质

1.添加观察者前后的类类型

2.动态子类验证

3.setter方法验证

4.消息转发

3.手动触发KVO

4.KVO常遇到的问题

1. 添加和移除观察者要成对出现

2. 使用正确的 KVO 选项

3. 在 observeValueForKeyPath方法中处理变化

4. KVO 与多线程

5. 观察集合属性

6. 避免嵌套的 KVO 通知

7. 使用上下文指针区分不同的 KVO 观察

8. 使用 NSKeyValueObservingOptions 获取更详细的信息

9. 调试 KVO


前言

    这篇博客主要介绍KVO的用法。

1.KVO的概念和基础用法

1.概念

        在 iOS 开发中,Key-Value Observing(KVO)是一种强大的机制,允许对象观察其他对象属性的变化。

2.基础用法

        KVO的基础用法如下:

  1. 添加观察者
  2. 实现观察者方法
  3. 移除观察者

        举一个我们经常使用的一个例子,比如说当前的控制器中有一个UITableView,我们需要监听UITableView滑动时候的偏移量(多一些动画处理,业务逻辑处理等等),这个时候我们可以按照如下的步骤设置KVO.

        我们调用下面的addObserver方法,这个方法传递三个参数,第一个参数是要观察的对象,如果是在当前类,我们就传递self,第二个参数是我们要监听的对象的值类型,一般我们监听NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld两个值,第三个参数为context为上下文,我们用来区别当前类中不同的监听对象。

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

        我们在合适的地方调用addObserver方法,代码如下:

#pragma mark -- KVO
- (void)kvoMethod{
    [self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

       然后我们在observerValueForKeyPath中处理自己的业务逻辑,代码如下,我们打印了一下UITableView的偏移量。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"contentOffset"]) {
        CGPoint oldOffset = [change[NSKeyValueChangeOldKey] CGPointValue];
        CGPoint newOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
        NSLog(@"contentOffset changed from %@ to %@", NSStringFromCGPoint(oldOffset), NSStringFromCGPoint(newOffset));
        
        // 在这里处理偏移量的变化
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

        在控制器销毁的时候,记得释放监听者。

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

        完整的代码如下(这里只粘贴了KVO部分的代码):

#pragma mark -- KVO
- (void)kvoMethod{
    [self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)dealloc{
    [self.tableView removeObserver:self forKeyPath:@"contentOffset"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"contentOffset"]) {
        CGPoint oldOffset = [change[NSKeyValueChangeOldKey] CGPointValue];
        CGPoint newOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
        NSLog(@"contentOffset changed from %@ to %@", NSStringFromCGPoint(oldOffset), NSStringFromCGPoint(newOffset));
        
        // 在这里处理偏移量的变化
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

2.KVO的本质

        在 iOS 中KVO 是通过动态子类化和消息转发来实现的。

        下面是对 KVO 的本质的详细解释:         

1.KVO 的工作原理

1. 动态子类化
           当你对某个对象的属性添加观察者时,KVO 会在运行时动态地创建该对象的一个子类,并将这个对象的 isa指针指向这个新的子类。
           这个子类重写了被观察属性的 setter 方法。重写后的 setter 方法在调用父类原始的 setter 方法之前和之后,分别调用 willChangeValueForKey: 和 didChangeValueForKey:方法。

2. willChangeValueForKey: 和 didChangeValueForKey
   willChangeValueForKey:在属性值改变之前调用,通知系统即将发生变化。
   didChangeValueForKey:在属性值改变之后调用,通知系统已经发生变化。
   - 这两个方法会触发 KVO 通知,观察者在 observeValueForKeyPath:ofObject:change:context: 方法中接收到这些通知,并进行相应的处理。

3. 消息转发
   - 新的子类会重写 class`方法,返回原始类的类型,使得对象看起来还是原来的类。
   - 当对该对象发送消息时,如果该消息不是由重写的方法处理的,消息会被转发到原始类的实现。

2.示例代码解释 KVO 的本质

我们可以通过查看对象的 `isa` 指针和类类型来观察 KVO 的工作原理。

#import "KVODemosVC.h"
#import <objc/runtime.h>
@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end

@interface KVODemosVC ()
@property (nonatomic, strong) Person *person;

@end

@implementation KVODemosVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.person = [[Person alloc] init];


    // 创建 UIButton
    UIButton *centerButton = [UIButton buttonWithType:UIButtonTypeSystem];
    centerButton.backgroundColor = [UIColor darkGrayColor];
    centerButton.layer.masksToBounds = YES;
    centerButton.layer.borderWidth = 1;
    centerButton.layer.cornerRadius = 10;
    [centerButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [centerButton setTitle:@"更新对象名称" forState:UIControlStateNormal];
    [centerButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];

    // 禁用自动调整掩码约束转换为自动布局约束
    centerButton.translatesAutoresizingMaskIntoConstraints = NO;

    // 将按钮添加到视图
    [self.view addSubview:centerButton];

    // 设置按钮的自动布局约束
    [NSLayoutConstraint activateConstraints:@[
        [centerButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [centerButton.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor],
        [centerButton.widthAnchor constraintEqualToConstant:150],
        [centerButton.heightAnchor constraintEqualToConstant:50]
    ]];
    NSLog(@"触发KVO之前类: %@", object_getClass(self.person));
    
    [self addObserverForPerson];


}

- (void)buttonTapped:(UIButton *)sender {
    NSLog(@"Button was tapped!");
    self.person.name = [self generateRandomStringWithLength:10];
    NSLog(@"触发KVO之后类: %@", object_getClass(self.person));
}

- (void)addObserverForPerson {
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        id oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        id newValue = [change objectForKey:NSKeyValueChangeNewKey];
        NSLog(@"监听的属性:%@\t监听的对象:%@\t 修改之前:%@\t 修改之后 %@\t", keyPath, object, oldValue, newValue);
        NSLog(@"person1类父类:%@",NSStringFromClass([[self.person class] superclass]));
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"移除KVO之后: %@", object_getClass(self.person));
}
- (NSString *)generateRandomStringWithLength:(NSInteger)length {
    NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    NSMutableString *randomString = [NSMutableString stringWithCapacity:length];

    for (NSInteger i = 0; i < length; i++) {
        u_int32_t rand = arc4random_uniform((u_int32_t)[letters length]);
        unichar c = [letters characterAtIndex:rand];
        [randomString appendFormat:@"%C", c];
    }

    return randomString;
}

@end

        在上面的实例代码中,我们在页面中监听person类的name属性变化,当我们点击按钮的时候,会随机生成一个字符串,然后把这个字符串赋值给我们要监听的person类的name属性。我们分别再出发KVO之前和之后打印person的类型。

        运行代码之后,控制台打印信息如下:

图1.KVO控制台打印信息

1.添加观察者前后的类类型

        为了验证这个问题,我们分别在添加KVO前后分别打印出person类型。

        从上述控制台的打印信息我们可以看到:
          在添加观察者之前,person 的类是 Person。
          在添加观察者之后,person 的类变成了一个新的子类,这个子类的名字通常是 _NSKVONotifying_Person。
        移除观察者后,person`的类又变回Person。

2.动态子类验证

        当 addObserver:forKeyPath:options:context: 被调用时,系统动态创建了一个新的子类 _NSKVONotifying_Person,并将 person 的 isa指针指向这个子类。

        我们KVO打印前后分别打印下person类的元类对象,会发现动态生成的_NSKVONotifying_Person的isa指针指向自己的元类对象。

3.setter方法验证

       在使用KVO的过程中,被观察的对象会调用自己的方法。

       然后我们打一个断点,使用lldb命令看一下添加KVO之前被观察对象的实现。

    NSLog(@"触发KVO之前set方法地址: %p", [self.person methodForSelector:@selector(setName:)]);

        控制台打印如下:

图2.添加KVO之前修改name属性的控制台打印信息

        从控制台打印信息来看,当我们修改person的name属性的时候调用的是person的setName方法。

        我们在触发KVO之后,也在控制台打印下person的实现过程:

        图3.添加KVO之后修改name属性的时候控制台打印信息   

        通过对比我们可以看到添加KVO之后,当我们修改name属性的时候,会调用Foundation框架的_NSSetObjectValueAndNotify方法。  

        在动态子类中,name属性的 setter 方法会在设置新值前后,分别调willChangeValueForKey:和 didChangeValueForKey:。

        为了验证这个结论,我们在Person的类中添加打印函数,看一下调用顺序:

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"==========>>>>>willChangeValueForKey被调用<<<<<==========");
}
- (void)setName:(NSString *)name{
    _name = name;
    NSLog(@"setName方法被调用");
}
- (void)didChangeValueForKey:(NSString *)key{
    [super didChangeValueForKey:key];
    NSLog(@"==========>>>>>didChangeValueForKey被调用<<<<<==========");
}

        运行代码之后,控制台打印信息如下:

        图4.控制台打印信息

        我们可以得出结论:当我们使用KVO的时候,首先会调用Foundation中的_NSSetObjectValueAndNotify方法,这个方法中首先会调用willChangeValueForKey方法,然后调用被观察的类的set方法修改属性,修改完成之后,调用didChangeValueForKey方法。

4.消息转发

        新的子类会拦截对被观察属性的setter调用,并进行处理。

        其他消息会被转发到原始类的实现。

        通过这些机制,KVO 能够在属性发生变化时通知观察者,实现属性观察的功能。

3.手动触发KVO

        了解KVO触发的机制之后,我们可以手动调用willChangeForKey和didChangeForKey即可:

- (void)buttonTapped:(UIButton *)sender {
    NSLog(@"Button was tapped!");
    
    // 手动触发 KVO
    [self.person willChangeValueForKey:@"name"];
    self.person.name = [self generateRandomStringWithLength:10];
    [self.person didChangeValueForKey:@"name"];
}

4.KVO常遇到的问题

        在使用KVO时,需要注意以下几个方面,以确保代码的安全性和正确性。

1. 添加和移除观察者要成对出现

        确保移除观察者:在对象的生命周期结束之前,一定要移除所有观察者。否则会导致应用崩溃。

    [self.person removeObserver:self forKeyPath:@"name"];

        常见位置:在 dealloc方法中移除观察者,确保对象销毁前已移除观察者。
 

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"移除KVO之后: %@", object_getClass(self.person));
}

2. 使用正确的 KVO 选项

常用选项:

        NSKeyValueObservingOptionNew:获取新值。

        NSKeyValueObservingOptionOld:获取旧值。
示例:

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

3. 在 observeValueForKeyPath方法中处理变化

        检查 keyPath:确保正确处理感兴趣的 keyPath。

  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
      if ([keyPath isEqualToString:@"name"]) {
          // 处理 name 属性的变化
      } else {
          [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
      }
  }

4. KVO 与多线程

        线程安全:KVO 不是线程安全的。如果观察的属性可能在多个线程中修改,确保在访问和修改属性时使用合适的同步机制。

  @synchronized(self) {
      self.person.name = @"NewName";
  }

5. 观察集合属性

        特殊方法:如果观察的是集合(如数组、集合等),使用 KVO的mutableArrayValueForKey: 方法来修改集合,以确保正确发送 KVO 通知。

NSMutableArray *names = [self mutableArrayValueForKey:@"names"];
  [names addObject:@"NewName"];

6. 避免嵌套的 KVO 通知

        避免循环**:在 KVO 通知中修改同一个属性时,要特别小心,避免触发嵌套的 KVO 通知,从而导致死循环。

  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
      if ([keyPath isEqualToString:@"name"]) {
          if (!self.isUpdating) {
              self.isUpdating = YES;
              // 修改属性的代码
              self.isUpdating = NO;
          }
      }
  }

7. 使用上下文指针区分不同的 KVO 观察

        使用上下文指针:当同一个对象观察多个属性时,可以使用上下文指针来区分不同的观察者。
 

  static void *PersonNameContext = &PersonNameContext;
  [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
  
  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey, id> *)change context:(void *)context {
      if (context == PersonNameContext) {
          // 处理 name 属性的变化
      } else {
          [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
      }
  }

8. 使用 NSKeyValueObservingOptions 获取更详细的信息

        获取更多信息:通过 `NSKeyValueObservingOptions` 可以获取属性变化的详细信息,如新值、旧值等。

  [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

9. 调试 KVO

        调试工具:使用 `NSLog` 打印属性变化的详细信息,有助于调试 KVO 相关的问题。

 NSLog(@"Old value: %@", [change objectForKey:NSKeyValueChangeOldKey]);
 NSLog(@"New value: %@", [change objectForKey:NSKeyValueChangeNewKey]);
  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫柱子哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值