iOS - 关于 KVO 的一些总结

网络配图.jpg

1. 什么是 KVO

  • KVO的全称是Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,容许一个对象观察/监听另外一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通讯的一种特别有用的技术。
  • KVONSNotificationCenter都是iOS中观察者模式的一种实现。
  • KVO能够监听单个属性的变化,也能够监听集合对象的变化。监听集合对象变化时,须要经过KVCmutableArrayValueForKey:等可变代理方法得到集合代理对象,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArrayNSSet
  • KVOKVC有着密切的关系,若是想要深刻了解KVO,建议先学习KVC
    传送门:iOS - 关于 KVC 的一些总结

2. KVO 的基本使用

KVO使用三部曲:添加/注册KVO监听、实现监听方法以接收属性改变通知、 移除KVO监听。html

  1. 调用方法addObserver:forKeyPath:options:context:给被观察对象添加观察者;
  2. 在观察者类中实现observeValueForKeyPath:ofObject:change:context:方法以接收属性改变的通知消息;
  3. 当观察者不须要再监听时,调用removeObserver:forKeyPath:方法将观察者移除。须要注意的是,至少须要在观察者销毁以前,调用此方法,不然可能会致使Crash

2.1 注册方法

/* ** target: 被观察对象 ** observer:观察者对象 ** keyPath: 被观察对象的属性的关键路径,不能为nil ** options: 观察的配置选项,包括观察的内容(枚举类型): NSKeyValueObservingOptionNew:观察新值 NSKeyValueObservingOptionOld:观察旧值 NSKeyValueObservingOptionInitial:观察初始值,若是想在注册观察者后,当即接收一次回调,能够加入该枚举值 NSKeyValueObservingOptionPrior:分别在值改变先后触发方法(即一次修改有两次触发) ** context: 能够传入任意数据(任意类型的对象或者C指针),在监听方法中能够接收到这个数据,是KVO中的一种传值方式 若是传的是一个对象,必须在移除观察以前持有它的强引用,不然在监听方法中访问context就可能致使Crash */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
 options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
复制代码

2.2 监听方法

若是对象被注册成为观察者,则该对象必须能响应如下监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。若是没有实现就会致使Crashgit

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* ** keyPath:被观察对象的属性的关键路径 ** object: 被观察对象 ** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回 key为 NSKeyValueChangeKey 枚举类型 { 1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key) { 对应枚举类型 NSKeyValueChange typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; 若是是对被观察对象属性(包括集合)进行赋值操做,kind 字段的值为 NSKeyValueChangeSetting 若是被观察的是集合对象,且进行的是(插入、删除、替换)操做,则会根据集合对象的操做方式来设置 kind 字段的值 插入:NSKeyValueChangeInsertion 删除:NSKeyValueChangeRemoval 替换:NSKeyValueChangeReplacement } 2.NSKeyValueChangeNewKey: 存储新值(若是options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key) 3.NSKeyValueChangeOldKey: 存储旧值(若是options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key) 4.NSKeyValueChangeIndexesKey:若是被观察的是集合对象,change字典中就会包含这个key, 这个key的value是一个NSIndexSet对象,包含更改关系中的索引 5.NSKeyValueChangeNotificationIsPriorKey:若是options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。 这个key对应的value是NSNumber包装的YES,咱们能够这样来判断是否是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES] } ** context:注册方法中传入的context */
}
复制代码

2.3 移除方法

在调用注册方法后,KVO并不会对观察者进行强引用,因此须要注意观察者的生命周期。至少须要在观察者销毁以前,调用如下方法移除观察者,不然若是在观察者被释放后,再次触发KVO监听方法就会致使Crashgithub

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
复制代码

2.4 使用示例

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.name= @"张三";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"name"];
}
复制代码

keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = ""; }
context:(null)编程

2.5 实际应用

KVO主要用来作键值观察操做,想要一个值发生改变后通知另外一个对象,则用KVO实现最为合适。斯坦福大学的iOS教程中有一个很经典的案例,经过KVOModelController之间进行通讯。如图所示: 数组

斯坦福大学 KVO示例

2.6 KVO触发监听方法的方式

KVO触发分为自动触发和手动触发两种方式。安全

2.6.1 自动触发

① 若是是监听对象特定属性值的改变,经过如下方式改变属性值会触发KVO网络

  • 使用点语法
  • 使用setter方法
  • 使用KVCsetValue:forKey:方法
  • 使用KVCsetValue:forKeyPath:方法

② 若是是监听集合对象的改变,须要经过KVCmutableArrayValueForKey:等方法得到代理对象,并使用代理对象进行操做,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含NSArrayNSSetapp

2.6.2 手动触发

① 普通对象属性或是成员变量使用:框架

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
复制代码

NSArray对象使用:ide

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码

NSSet对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码

3. KVO 的进阶使用

3.1 observationInfo 属性

  • observationInfo属性是NSKeyValueObserving.h文件中系统经过分类给NSObject添加的属性,因此全部继承于NSObject的对象都含有该属性;
  • 能够经过observationInfo属性查看被观察对象的所有观察信息,包括observerkeyPathoptionscontext等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
复制代码

3.2 context 的使用

注册方法addObserver:forKeyPath:options:context:中的context能够传入任意数据,而且能够在监听方法中接收到这个数据。

  • context做用:标签-区分,能够更精确的肯定被观察对象属性,用于继承、 多监听;也能够用来传值。
      KVO只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,咱们一般状况下能够在注册方法中指定contextNULL,并在监听方法中经过objectkeyPath来判断触发KVO的来源。
      可是若是存在继承的状况,好比如今有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察。问题:
      ① 当 balance 发生改变时,应该由谁来处理呢?
      ② 若是都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是本身的事务仍是子类对象的事务呢?
      这时候经过使用context就能够很好地解决这个问题,在注册方法中为context设置一个独一无二的值,而后在监听方法中对context值进行检验便可。

  • 苹果的推荐用法:用context来精确的肯定被观察对象属性,使用惟一命名的静态变量的地址做为context的值。能够为整个类设置一个context,而后在监听方法中经过objectkeyPath来肯定被观察属性,这样存在继承的状况就能够经过context来判断;也能够为每一个被观察对象属性设置不一样的context,这样使用context就能够精确的肯定被观察对象属性。

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];
}
复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something with the balance…
 
    } 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];
    }
}
复制代码
  • context优势:嵌套少、性能高、更安全、扩展性强。
  • context注意点:
    ① 若是传的是一个对象,必须在移除观察以前持有它的强引用,不然在监听方法中访问context就可能致使Crash
    ② 空传NULL而不该该传nil

3.3 KVO监听集合对象

KVO能够监听单个属性的变化,也能够监听集合对象的变化。监听集合对象变化时,须要经过KVCmutableArrayValueForKey:等方法得到代理对象,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArrayNSSet。 (注意:若是直接对集合对象进行操做改变,不会触发KVO。)

示例代码及输出以下:

观察者 viewController 对被观察对象 person 的 mArray 属性进行监听。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    self.person.mArray = [NSMutableArray arrayWithCapacity:5];
    [self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person.mArray addObject:@"2"]; //若是直接对数组进行操做,不会触发KVO
    NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
    [array addObject:@"1"];
    [array replaceObjectAtIndex:0 withObject:@"2"];
    [array removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    /* change 字典的值为: { indexes:对应的值为数组操做的详细信息,包括索引等 kind: 对应的值为数组操做的方式: 2:表明插入操做 3:表明删除操做 4:表明替换操做 typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4, }; new/old:若是是插入操做,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew) 若是是删除操做,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld) 若是是替换操做,则字典中new和old字段均可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = ( 1 ); } */  
    NSLog(@"%@",change);  
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"mArray"];
}
复制代码

{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }

3.4 KVO 的自动触发控制

  能够在被观察对象的类中重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法来控制KVO的自动触发。
  若是咱们只容许外界观察 person 的 name 属性,能够在 Person 类以下操做。这样外界就只能观察 name 属性,即便外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发KVO

// 返回值表明允不容许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"name"]) {
        automatic = YES;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}
复制代码

  也能够实现+ (BOOL)automaticallyNotifiesObserversOf<Key>方法来单一控制属性的KVO自动触发,<Key>为属性名(首字母大写)。

+ (BOOL)automaticallyNotifiesObserversOfName
{
    return NO;
}
复制代码

注意:

  • 第一个方法的优先级高于第二个方法。若是实现了automaticallyNotifiesObserversForKey:方法,并对<Key>作了处理,则系统就不会再调用该<Key>automaticallyNotifiesObserversOf<Key>方法。
  • options指定的NSKeyValueObservingOptionInitial触发的KVO通知,是没法被automaticallyNotifiesObserversForKey:阻止的。

3.5 KVO 的手动触发

使用场景:

  • 使用KVO监听成员变量值的改变;
  • 在某些须要控制监听过程的场景下。好比:为了尽可能减小没必要要的触发通知操做,或者当多个更改同时具有的时候才调用属性改变的监听方法。

  因为KVO的本质,重写setter方法来达到能够通知全部观察者对象的目的,因此只有经过setter方法或KVC方法去修改属性变量值的时候,才会触发KVO,直接修改为员变量不会触发KVO
  当咱们要使用KVO监听成员变量值改变的时候,能够经过在为成员变量赋值的先后手动调用willChangeValueForKey:didChangeValueForKey:两个方法来手动触发KVO,如:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"age"];
    self.person->_age = 18;
    [self.person didChangeValueForKey:@"age"];
}
复制代码

  NSKeyValueObservingOptionPrior(分别在值改变先后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:didChangeValueForKey:的时候进行的。
  若是注册方法中options传入NSKeyValueObservingOptionPrior,那么能够经过只调用willChangeValueForKey:来触发改变前的那次KVO,能够用于在属性值即将更改前作一些操做。

3.6 KVO 新旧值相等时不触发

  有时候咱们可能会有这样的需求,KVO监听的属性值修改先后相等的时候,不触发KVO的监听方法,能够结合KVO的自动触发控制和手动触发来实现。
  例如:对 person 对象的 name 属性注册了KVO监听,咱们但愿在对 name 属性赋值时作一个判断,若是新值和旧值相等,则不触发KVO,能够在 Person 类中以下这样实现,将 name 属性值改变的KVO触发方式由自动触发改成手动触发。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = YES;
    if ([key isEqualToString:@"name"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)setName:(NSString *)name
{
    if (![_name isEqualToString:name]) {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    } 
}
复制代码

3.7 KVO 手动观察集合属性

有些状况下咱们想手动观察集合属性,下面以观察数组为例。
关键方法:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
复制代码

须要注意的是,根据KVCNSMutableArray 搜索模式
传送门:iOS - 关于 KVC 的一些总结

  • 至少要实现一个插入和一个删除方法,不然不会触发KVO。如 插入方法:insertObject:in<Key>AtIndex:insert<Key>:atIndexes: 删除方法:removeObjectFrom<Key>AtIndex:remove<Key>AtIndexes:
  • 能够不实现替换方法,可是若是不实现替换方法,执行替换操做时,KVO会把它当成先删除后添加,即会触发两次KVO。第一次触发的KVOchange字典的old键的值为替换前的元素,第二次触发的KVOchange字典的new键的值为替换后的元素,前提条件是注册方法中的options传入对应的枚举值。
  • 若是实现替换方法,则执行替换操做只会触发一次KVO,而且change字典会同时包含newold,前提条件是注册方法中的options传入对应的枚举值。 替换方法:replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:

示例代码以下:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"mArray"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}

- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray insertObjects:array atIndexes:indexes];

    [self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray removeObjectsAtIndexes:indexes];

    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}

- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
    [self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];

    [self.mArray replaceObjectsAtIndexes:indexes withObjects:array];

    [self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
复制代码

3.8 KVO 的依赖观察

3.8.1 一对一关系

  有些状况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
  好比咱们想要对 Download 类中的 downloadProgress 属性进行KVO监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。如下有两种方法能够解决这个问题。

  1. 重写如下方法来指明 downloadProgress 属性依赖于 writtenData 和 totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"writtenData",@"totalData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
复制代码
  1. 实现一个遵循命名规则为keyPathsForValuesAffecting<Key>的类方法,<Key>是依赖于其余值的属性名(首字母大写):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
    return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
复制代码

注意: 以上两个方法能够同时存在,且都会调用,可是最终结果会以keyPathsForValuesAffectingValueForKey:为准。

3.8.2 一对多关系

  以上方法在观察集合属性时就无论用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你但愿 Department 类有一个 totalSalary 属性来计算全部员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于全部 Employee 实例对象的 salary 属性。如下有两种方法能够解决这个问题。

  1. 你能够用KVO将 parent(好比 Department )做为全部 children(好比 Employee )相关属性的观察者。你必须在把 child 添加或删除到 parent 时把 parent 做为 child 的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:方法中咱们能够针对被依赖项的变动来更新依赖项的值:
#import "Department.h"

static void *totalSalaryContext = &totalSalaryContext;

@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;

@end


@implementation Department

- (instancetype)initWithEmployees:(NSArray *)employees
{
    self = [super init];
    if (self) {
        self.employees = [employees copy];
        for (Employee *em in self.employees) {
            [em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
        }
    }
    return self;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    
}
 
- (void)setTotalSalary:(NSNumber *)totalSalary
{
    if (_totalSalary != totalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = totalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}

- (void)dealloc
{
    for (Employee *em in self.employees) {
        [em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
    }
}

@end

复制代码
  1. 使用iOS中观察者模式的另外一种实现方式:通知 (NSNotification) 。

4. KVO的使用注意

4.1 移除观察者的注意点

  • 在调用KVO注册方法后,KVO并不会对观察者进行强引用,因此须要注意观察者的生命周期。至少须要在观察者销毁以前,调用KVO移除方法移除观察者,不然若是在观察者被释放后,再次触发KVO监听方法就会致使Crash
  • KVO的注册方法和移除方法应该是成对的,若是重复调用移除方法,就会抛出异常NSRangeException并致使程序Crash
  • 苹果官方推荐的方式是,在观察者初始化期间(init或者viewDidLoad的时候)注册为观察者,在释放过程当中(dealloc时)调用移除方法,这样能够保证它们是成对出现的,是一种比较理想的使用方式。

4.2 防止屡次注册和移除相同的KVO

  有时候咱们难以免屡次注册和移除相同的KVO,或者移除了一个未注册的观察者,从而产生可能会致使Crash的风险。
  三种解决方案:黑科技防止屡次添加删除KVO出现的问题

  • 利用 @try @catch(只能针对删除屡次KVO的状况下) 给NSObject增长一个分类,而后利用Runtime API交换系统的removeObserver方法,在里面添加@try @catch
  • 利用 模型数组 进行存储记录
  • 利用 observationInfo 里私有属性

4.3 其它注意点

  • 若是对象被注册成为观察者,则该对象必须能响应监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。若是没有实现就会致使Crash。因此KVO三部曲缺一不可。
  • keyPath传入的是一个字符串,为避免写错,可使用NSStringFromSelector(@selector(propertyName)),将属性的getter方法SEL转换成字符串,在编译阶段对keyPath进行检验。
  • 若是注册方法中context传的是一个对象,必须在移除观察以前持有它的强引用,不然在监听方法中访问context就可能致使Crash
  • 若是是监听集合对象的改变,须要经过KVCmutableArrayValueForKey:等方法得到代理对象,并使用代理对象进行操做,当代理对象的内部对象发生改变时,会触发KVO。若是直接对集合对象进行操做改变,不会触发KVO
  • 在观察者类的监听方法中,应该为没法识别的context或者objectkeyPath调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

5. KVO的实现原理

Key-Value Observing Implementation Details

  • Automatic key-value observing is implemented using a technique called isa-swizzling.
  • The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
  • When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
  • You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

  以上是苹果官方对KVO实现的解释,只说明了KVO是使用isa-swizzling技术来实现的,并无作过多介绍。

5.1 isa-swizzling

  苹果使用了isa混写技术(isa-swizzling)来实现KVO。当咱们调用了addObserver:forKeyPath:options:context:方法,为instance被观察对象添加KVO监听后,系统会在运行时利用Runtime API动态建立instance对象所属类A的子类NSKVONotifying_A,而且让instance对象的isa指向这个全新的子类,并重写原类A的被观察属性的setter方法来达到能够通知全部观察者对象的目的。
  这个子类的isa指针指向它本身的meta-class对象,而不是原类的meta-class对象。
  重写的setter方法的SEL对应的IMPFoundation中的_NSSetXXXValueAndNotify函数(XXXKey的数据类型),当被观察对象的属性发送改变时,会_NSSetXXXValueAndNotify函数,这个函数中会调用:

  • willChangeValueForKey:方法
  • 父类原来的setter方法
  • didChangeValueForKey:方法(内部会触发监听器即观察对象observer的监听方法:observeValueForKeyPath:ofObject:change:context:

  在移除KVO监听后,被观察对象的isa会指回原类A,可是NSKVONotifying_A类并无销毁,还保存在内存中。

5.2 KVO 动态生成的子类都有哪些方法

  NSKVONotifying_A除了重写了setter方法,还重写了classdealloc_isKVOA这三个方法(可使用runtimeclass_copyMethodList函数打印方法列表得到),其中:

  • classclass方法中返回的是父类的class对象,目的是为了避免让外界知道KVO动态生成类的存在;
  • dealloc:释放KVO使用过程当中产生的东西;
  • _isKVOA:用来标志它是一个KVO的类。

6. FBKVOController

6.1 系统 KVO 的缺点

  • 使用比较麻烦,须要三个步骤:添加/注册KVO监听、实现监听方法以接收属性改变通知、 移除KVO监听,缺一不可;
  • 须要手动移除观察者,移除观察者的时机必须合适,还不能重复移除;
  • 注册观察者的代码和事件发生处的代码上下文不一样,传递上下文context是经过void *指针;
  • 须要实现-observeValueForKeyPath:ofObject:change:context:方法,比较麻烦;
  • 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,须要在方法中写大量的if进行判断。

6.2 FBKVOController 的介绍

FBKVOController是 Facebook 开源的一个基于系统KVO实现的框架。支持Objective-CSwift语言。
GitHub:github.com/facebook/KV…

6.3 FBKVOController 的优势

  • 会自动移除观察者;
  • 函数式编程,能够一行代码实现系统KVO的三个步骤;
  • 实现KVO与事件发生处的代码上下文相同,不须要跨方法传参数;
  • 增长了blockSEL自定义操做对NSKeyValueObserving回调的处理支持;
  • 每个keyPath会对应一个block或者SEL,不须要使用if判断keyPath
  • 能够同时对一个对象的多个属性进行监听,写法简洁;
  • 线程安全。

6.4 FBKVOController 的使用

FBKVOController实现了观察者和被观察者的角色反转,系统的KVO是被观察者添加观察者,而FBKVO实现了观察者主动去添加被观察者,实现了角色上的反转,使用比较方便。

// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
复制代码

6.5 FBKVOController 的解析

如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)

参考

Key-Value Observing Programming Guide(苹果官方文档)
iOS - 关于 KVC 的一些总结(掘金)
KVO原理分析及使用进阶(简书)
iOS开发 - 黑科技防止屡次添加删除KVO出现的问题(简书)
谈谈 KVO(简书)
GitHub/facebook/KVOController(GitHub)
如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)

相关文章
相关标签/搜索