iOS底层--KVO(一)-基础知识点

KVO: 全称--- Key-value observing

探索KVO原理和KVO一样,通过官方文档去看。

KVO使用流程

简单三步骤:

1、注册观察者
2、实现方法来获取观察者属性改变的通知
3、移除观察者

  • 官方文档的介绍
You must perform the following steps to enable an object to receive key-value observing notifications for a KVO-compliant property:
必须执行以下步骤,才能使对象接收KVO兼容属性通知的键值:

*   Register the observer with the observed object using the method  addObserver:forKeyPath:options:context:
    将观察者注册到观察对象上 使用这个方法:addObserver:forKeyPath:options:context:
*   Implement  observeValueForKeyPath:ofObject:change:context:  inside the observer to accept change notification messages.
    实现 observeValueForKeyPath:ofObject:change:context:  来接收观察者内部值的变化的通知消息。
*   Unregister the observer using the method removeObserver:forKeyPath: when it no longer should receive messages. At a minimum, invoke this method before the observer is released from memory.
    在观察者从内存释放之前,调用removeObserver:forKeyPath:来移除观察者
  • 官方示例
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];
    }
}

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

参数的意义

1、 context

context 在官方文档有介绍:

The context pointer in the `addObserver:forKeyPath:options:context:` message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify `NULL` and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.

The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the `balance` and `interestRate` properties chosen this way.

**Listing 1**  Creating context pointers

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

content 相对于keyPath,更安全、便利、直接,更好扩展的方式来区分接收到的消息来源于哪个对象

context 包含将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖keyPath字符串来确定通知的来源,但是这种方式可能会导致一个问题,父类的一个对象由于不同的原因也在观察同一个路径。

nil、Nil、NULL的区别


2、 Options
  • NSKeyValueObservingOptionNew: 变化之后的值
  • NSKeyValueObservingOptionOld:变化之前的值
  • NSKeyValueObservingOptionInitial: 作用是 注册观察者的时候,立即向观察者发送通知(添加观察,就会调用一次回调)。
    如果同时有OptionOld的情况下,第一次回调是没有old value的
  • NSKeyValueObservingOptionPrior:在value发生变化之前发送一次通知,value变化之后正常发送一次通知。(也就是willChange 和 didChange都发送通知)。
    在变化前的通知(第一次通知)中,会包含NSKeyValueChangeNotificationIsPriorKey ,不包含NewKey(即使观察OptionNew,在第一次通知中,newKey也会被丢弃)

3、 NSKeyValueChangeKey
  • NSKeyValueChangeKindKey 触发KVO的来源

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1, //调用set方法

    // 观测的值是一个可变集合/数组的情况
    NSKeyValueChangeInsertion = 2, //插入操作
    NSKeyValueChangeRemoval = 3, //移除操作
    NSKeyValueChangeReplacement = 4, // 替换操作
    };

  • NSKeyValueChangeNewKey: 新值的key
    ⚠️:当KindKey 是2/3/4的时候,newKey是变化的那个元素,而不是变化后的数组。具体查看 注意点-4

  • NSKeyValueChangeIndexesKey: 对数组操作时(NSKeyValueChange 不是 1 的情况下)有效 indexes="<_NSCachedIndexSet: 0x7fde0a50cd20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"

  • NSKeyValueChangeNotificationIsPriorKey: 观察Options为OptionPrior的情况下,发送第一次(willChange)通知的时候有效


其他知识点

1、自动调用/手动调用

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
是否开启自动调用。

  • 自动调用:系统在合适的地方会自动添加 willChange和didChange,在这种情况下触发的,称之为自动调用
  • 手动调用:有时候在重写set方法的时候,会添加 willChange和didChange方法(最底下 KVO注意点-第三点),自己添加change方法的情况下触发的KVO称之为手动调用。

还记得在KVC原理里面,有提到一个 是否自动去匹配相关的成员变量的方法:accessInstanceVariablesDirectly 有些类似,都是一个开关方法。


2、多因素影响(比如下载进度)

下载进度的多少 取决于 总下载数据量和已下载数据量

@interface Person : NSObject
@property (nonatomic, assign) double getData;   //以获取data
@property (nonatomic, assign) double totalData; //总data
@property (nonatomic, assign) double progress;  //进度
@end

@implementation Person
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"progress"]) {
        NSArray *affectingKeys = @[@"totalData", @"getData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
@end


//ViewController.m
_person = [Person new];
_person.totalData = 100;
[self addKVOCHild];

- (void)addKVOCHild {
    [_person addObserver:self forKeyPath:@"progress"
    options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:viewkeyPaht];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    
    if (context == viewkeyPaht) {
        NSLog(@"progress = %f",_person.getData/_person.totalData);
    }else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    _person.getData += 20;
    _person.totalData += 15;
}

//每次touch,会回调2次  因为getData、totalData 都会影响progress

3、数组KVO触发方式

对数组操作的普通写法 添加、删除、替换:[_person.dateArray addObject:@"1"];无法触发KVO。
如果要在add、replace、remove的时候,可以触发KVO,需要借助KVC的方式
[[_person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

注意触发KVO的change:kind 不再是1了
查看 ---< 注意点-4 > -----

KVO注意点

  • 1、通常在接收通知消息中,对观察对象进行处理,没有处理的应该通过super observeValueForKeyPath ....的方式传递到上一层。
    这里就存在一个问题:如果父类、父类的父类...都没有处理,就会抛出NSInternalInconsistencyException异常:message was received but not handled,所以,你自己添加的观察对象,你需要自己处理

  • 2、注册观察者的时候如果context不是NULL,那么在移除观察的时候,应该使用 removeObserver: forKeyPath: context: 方法去移除。
    详细原因可查看addObserver:forKeyPath:options: context:的注释。简单的说,就是:当同一个观察者多次注册到同一个keyPath,但每次使用context时,-removeObserver:forKeyPath:在决定要删除的内容时,他会猜测应该移除哪一个context,它可能猜错了。

  • 3、对于添加的属性,如果需要对其进行观察,在set方法中 不应该手动加入willchangedidChange方法,因为这样会导致接收消息触发2次,
    系统在生成set方法的时候,会在合适的时候自动添加ChangeValue方法,即使重写了set方法,依然不需要手动添加
    在分类添加属性并重写set方法的时候,也是不需要的。

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];    //这一行是不需要的
    _name = name;
    [self didChangeValueForKey:@"name"];    //这一行是不需要的
}

那么ChangeValue是在什么情况下用的呢? 是在不调用set方法的情况下,想手动触发KVO消息。

  • 4、对于数组的KVO,使用[[_testModel mutableArrayValueForKey:@"childArr"] removeObject:@"1"];这个方法才能触发KVO,但是还有一点需要注意,KVO观察到的NSKeyValueChangeNewKey的值,并不是 childArr的全部内容,而是变化的那个元素。比如一下代码:
static void *viewkeyPaht = &viewkeyPaht;
{
    _testModel.childArr = @[@"1",@"2",@"3"].mutableCopy;
    [_testModel addObserver:self forKeyPath:@"childArr" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [[_testModel mutableArrayValueForKey:@"childArr"] addObject:@"1"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == viewkeyPaht) {
        NSLog(@"change = %@",change);
    }else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
// 在touchesBegan 方法里进行remove、add、replace操作 分别打印结果
// addObject
change = {
    indexes = "<_NSCachedIndexSet: 0x600003df0900>[number of indexes: 1 (in 1 ranges), indexes: (3)]";
    kind = 2;
    new =     (
        4
    );
}

//  remove
change = {
    indexes = "<_NSCachedIndexSet: 0x600001b772a0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 3;
    old =     (
        2
    );
}

// replace
change = {
    indexes = "<_NSCachedIndexSet: 0x6000032e93a0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 4;
    new =     (
        4
    );
    old =     (
        1
    );
}

从上面的打印结果看,对数组操作,new和old都是针对变化的那个元素,而不是变化前后数组的值
还有一点注意到:
addObject 操作:OldKey被抛弃
remove 操作:NewKey被抛弃

你可能感兴趣的:(iOS底层--KVO(一)-基础知识点)