iOS - Key Value Observing

Key Value Observing (KVO) - 允许将其他对象的指定属性变更通知给对象


参考链接

  • Key-Value Observing.Apple.Documentation

一、At a Glance

KVO 主要用于 ModelController 之间的通信

1. View 通过 Controller 观察 Model 的属性进行改变
2. Model 也可以观察其他 Model,甚至可以观察 Model 本身(通常用于确认从属值何时改变)

>> Example

image.png
  • 假设 Person 实例与 Account 实例进行交互,表示该人在银行的储蓄账户。Person 实例可能需要知道 Account 实例的某些方面何时发生改变

  • Person 可以定期轮询 Account 的属性 blanceinterestRate,但是这样效率低下,所以衍生出 KVO,能够在 Account 发生改变时收到通知并作出响应

二、Registering for Key-Value Observing

  • 将观察者注册到观察对象 addObserver:forKeyPath:options:context:
  • observeValueForKeyPath:ofObject:change:context: 在观察者内部实现该方法以接收更改通知消息
  • 当观察者不再接收消息,应该使用该方法注销观察者 removeObserver:forKeyPath:,并在观察者释放之前调用该方法

并非所有的类都支持 KVO

1. Registering as an Observer

addObserver:forKeyPath:options:context: 注册成为观察者

不强引用观察者、被观察者和上下文

Options Description
NSKeyValueObservingOptionOld 指定 change dictionary 包含变更后的旧值
NSKeyValueObservingOptionNew 指定 change dictionary 包含变更后的新值
NSKeyValueObservingOptionInitial 在注册方法 return 之前发送一个通知哦
NSKeyValueObservingOptionPrior 注册后发送一个预改变通知,change dictionary 中包括 NSKeyValueChangeNotificationIsPriorKey : @(YES)

context:
可以为每个观察到的键路径创建一个上下文,区分父类与该类观察相同键路径的情况

/// Creating context pointers
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

- (void)registerAsObserverForAccount:(Account*)account {
    [account addObserver:self
              forKeyPath:@"balance"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                 context:PersonAccountBalanceContext];
 
    [account addObserver:self
              forKeyPath:@"interestRate"
                 options:(NSKeyValueObservingOptionNew |
                          NSKeyValueObservingOptionOld)
                  context:PersonAccountInterestRateContext];
}

2. Receiving Notification of a Change

Change Dictionary Description
NSKeyValueChangeKindKey NSKeyValueChangeSetting 如果观察到对象的值已更改
NSKeyValueChangeNewKey 提供更改之后的新值
NSKeyValueChangeOldKey 提供更改之前的旧值
NSKeyValueChangeIndexesKey 对应是一个 NSIndexSet 对象, 如果 NSKeyValueChangeKindKey 对应的键是 NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, or NSKeyValueChangeReplacement,那么该值为插入、移除、替换的对象
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…

       // if ([keyPath isEqualToString: @"keyPath"]) 
      /// 如果通过 NULL 指定上下文,则通过通知的 keypath 和 正在观察的 keypath 进行比较;

    } else if (context == PersonAccountInterestRateContext) {
        // Do something with the interest rate…
 
    } else {
        // Any unrecognized context must belong to super
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                               context:context];
    }
}

3. Removing an Object as an Observer

- (void)unregisterAsObserverForAccount:(Account *)account {
    [account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
    [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];
}

在移除观察者的时候需要注意以下几点

  • 如果在观察者移除时之前没有注册过,将会抛出异常
    请确保 removeObserver:forKeyPath:context:addObserver:forKeyPath:options:context: 是成对调用的;如果不能保证需要放在 try/catch 中处理可能存在的异常
  • 观察者不会在 deallocated 时自动移除自己,如果在观察者已被释放的情况下,被观察者发送变更通知,相当于发送到一个 released 对象,触发内存访问异常
  • 该协议无法查询对象是观察者和非观察者;经典做法是在观察者初始化期间(例如 init 或者 viewDidLoad)注册成为观察者,在释放过程中(dealloc)注销(注意 1: 1 配对添加和移除消息)

三、Registering Dependent Keys

1. To-One Relationships

一个属性的值取决于另一个对象中一个或多个其他属性的值

keyPathsForValuesAffectingValueForKey:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

firstNamelastName 发生改变时,需要通知观察 fullName 属性

/// Example 1
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

/// Example 2
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

2. To-Many Relationships

假如有一个对象 Department,他有很多 employees;然后每一个 employee 都具有 salary 属性
我们需要通过所有 employee.salary 计算出 totalSalary
一对多(一个父项对应多个子项)

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

如果使用的是 Core Data,可以在通知中心将父项注册为 Managed object 上下文的观察者,响应子项发布的相关变更通知

四、Key-Value Observing Implementation Details

  • 使用 isa-swizzling 技术;在对对象的属性注册观察者时,将修改观察对象的 isa 指针,指向一个中间类而不是真实的类,isa 指针的值不一定反映实例的实际类型
  • isa 指向对象的类,实际上是一个调度表,包含指向该类实现的方法的指针及其他数据
  • 不要依靠 isa 指针来确定类成员,相反应该使用 class 方法来确定对象实例的类

你可能感兴趣的:(iOS - Key Value Observing)