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

2020-07-09  本文已影响0人  Engandend

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

3、 NSKeyValueChangeKey

其他知识点

1、自动调用/手动调用

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

还记得在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<NSString *> *)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<NSKeyValueChangeKey,id> *)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<UITouch *> *)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注意点

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

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

static void *viewkeyPaht = &viewkeyPaht;
{
    _testModel.childArr = @[@"1",@"2",@"3"].mutableCopy;
    [_testModel addObserver:self forKeyPath:@"childArr" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)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被抛弃

上一篇 下一篇

猜你喜欢

热点阅读