【iOS】KVO详细总结

19 篇文章 0 订阅
10 篇文章 0 订阅

KVO

0.怎么复习?

  1. 知道KVO怎么用,KVO三部曲缺一不可
  2. KVO分为对单一属性的监听,和对集合里面内容的监听。然后单一属性的监听分为自动触发和手动触发,集合对象的监听也分为自动触发和手动触发
  3. KVO的使用注意,防crash。正确顺序:1⃣️context强引用2⃣️removeObserver移除观察者3⃣️释放observer观察者对象
    • 先removeObserver移除观察者,再释放observer观察者对象
    • context必须是强引用,才能removeObserver移除观察者
  4. KVO实现原理 isa-swizzling
  5. FBKVOController(方便的使用KVO,了解即可)

1. 什么是KVO

  • KVO的全称是Key-Value Observing,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。
  • KVO和NSNotification都是iOS中观察者模式的一种实现。
  • KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey:等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray和NSSet。
  • KVO和KVC有着密切的关系,如果想要深入了解KVO,建议先学习KVC。

2. KVO基本使用

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

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

2.1 注册方法

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

各参数讲解:

    1. observer:观察者对象
    1. keyPath: 被观察对象的属性的关键路径,不能为nil
    1. options: 观察的配置选项,包括观察的内容(枚举类型):
    • NSKeyValueObservingOptionNew:观察新值
    • NSKeyValueObservingOptionOld:观察旧值
    • NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
    • NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)
    1. context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式。如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash。

2. 2监听方法

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


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

}

    1. keyPath:被观察对象的属性的关键路径
    1. object: 被观察对象
    1. change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回
    1. key :
             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]
             }
    1. context:注册方法中传入的context

2.3移除方法

在调用注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

2.4使用示例

以下使用KVO, 为person对象添加观察者–“当前viewController”,监听person对象的name属性值的改变。当name值改变时,触发KVO的监听方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [HTPerson new];
    
    //1.为person类的name属性,添加观察者对象-当前ViewController
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld) context:NULL];
}

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

// 2.实现监听方法 
- (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);
}

// 3.释放观察者
- (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之间进行通信。如图所示:

在这里插入图片描述

2.6 KVO 触发监听方法的方式

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

2.6.1 自动触发

① 如果是**监听对象特定属性值的改变**,通过以下方式改变属性值会触发KVO:

  • 使用点语法
  • 使用setter方法
  • 使用KVC的setValue:forKey:方法
  • 使用KVC的setValue:forKeyPath:方法

② 如果是**监听集合对象的改变**,需要通过KVC**mutableArrayValueForKey:等方法**获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。集合对象包含NSArrayNSSet

2.6.2 手动触发

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

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

② NSArray对象使用:

- (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。

示例代码及输出如下:


- (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.5KVO的手动触发

使用场景:

  • 使用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"];
}

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;

需要注意的是,根据KVC的NSMutableArray 搜索模式:

  • 至少要实现一个插入和一个删除方法,否则不会触发KVO。如
    插入方法insertObject:in<Key>AtIndex:或insert<Key>:atIndexes:
    删除方法removeObjectFrom<Key>AtIndex:或remove<Key>AtIndexes:
  • 可以不实现替换方法,但是如果不实现替换方法,执行替换操作时,KVO会把它当成先删除后添加,即会触发两次KVO。第一次触发的KVO中change字典的old键的值为替换前的元素,第二次触发的KVO中change字典的new键的值为替换后的元素,前提条件是注册方法中的options传入对应的枚举值。
  • 如果实现替换方法,则执行替换操作只会触发一次KVO,并且change字典会同时包含new和old,前提条件是注册方法中的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 的依赖观察

1.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的类方法,是依赖于其他值的属性名(首字母大写):

+ (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) 。

KVO 的使用注意(KVO的Crush)

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
    - 可以使用__bridge_retained桥接剥夺对象的内存管理权,但必须记得在不需要该对象时释放它,否则内存泄露。关于桥接可以参阅《iOS - 老生常谈内存管理(三):ARC 面世 —— Toll-Free Bridging》;
    - 或者使用全局变量。
  • 如果是监听集合对象的改变,需要通过KVCmutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO。如果直接对集合对象进行操作改变,不会触发KVO
  • 在观察者类的监听方法中,应该为无法识别的context或者objectkeyPath 调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

5. KVO 的实现原理

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函数(XXX为Key的数据类型),当被观察对象的属性发送改变时,会调用_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进行判断。

FBKVOController 的介绍

FBKVOController是 Facebook 开源的一个基于系统KVO实现的框架。支持Objective-CSwift语言。

6.3 FBKVOController 的优点

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

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

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值