iOS开发技巧

OC底层原理18-KVO底层原理

2020-11-02  本文已影响0人  夏天的枫_

iOS--OC底层原理文章汇总

KVO(Key-Value Observing)——键值观察,它是一种机制,它允许将其他对象的指定属性的更改,通知给另一个对象。KVO苹果文档

关于KVO如何创建使用,大致分为三个步骤:

使用步骤

注册观察者

// 定义两个上下文
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];
}

options 是一个枚举,包含了以下四个值:
NSKeyValueObservingOptionNew:观察更改后的值;
NSKeyValueObservingOptionOld:观察更改前的值;
NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法);
NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)

context:上下文,包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以指定NULL并完全依赖KeyPath字符串来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因而观察到相同的键路径,从而导致问题。

一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。在类中定义唯一命名的静态变量的地址,就满足了良好的上下文条件。在父类或子类中以类似方式选择的上下文不太可能重叠。可以为整个类选择一个上下文,然后依靠通知消息中的KeyPath字符串来确定更改的内容。另外,可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析。上面示例中显示了以这种方式选择的balanceinterestRate属性的示例上下文。

接受变更通知

- (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];
    }
}

当对象的观察属性的值更改时,观察者会收到一条observeValueForKeyPath:ofObject:change:context: 消息。所有观察者都必须实现此方法。

移除观察者

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

典型的使用场景是在观察者初始化期间(例如,在initviewDidLoad)注册为观察者,在释放过程中(通常在中dealloc)解除注册,以确保成对和有序地添加和删除消息,并确保观察者在从内存中释放之前被取消注册。
如果注册了观察者未注销,当再次进入观察者界面时,会再次注册KVO观察者,导致KVO观察的重复注册,而第一次的通知对象还在内存中,没有进行释放。如果此时接收到了属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,将会导致类似野指针的崩溃,可理解为一直保持着一个野通知,且一直在监听。

问:多次添加注册未注销会不会造成循环引用?不会,因为observer在底层的字符串是weak修饰,所以不会导致循环引用。

自动 & 手动变更通知

自动变更通知

NSObject提供自动的键值更改通知的基本实现。自动键值更改通知将使用键值兼容访问器(setName)以及键值编码方法(setValue:forKey:)进行的更改通知给观察者。由mutableArrayValueForKey:返回的收集代理对象也支持自动通知。
以下显示的示例使该属性的所有观察者都name收到有关更改的通知。

// 使用setter方法直接设置
[account setName:@"Savings"];
// 使用kvc设置name
[account setValue:@"Savings" forKey:@"name"];
 
// 使用keypath 设置document的name
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// 使用 mutableArrayValueForKey: to retrieve a relationship proxy object.
NSArray * arrayTrans = @{@"1001",@"1002"};
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject: arrayTrans];

手动变更通知

这是切换手动or自动的方法,默认YES即为自动变更通知,这里可以判断theKey来控制是否手动变更通知。

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

要实现手动观察者通知,在willChangeValueForKey:更改值之前和didChangeValueForKey:更改值之后调用。如下实现了该balance属性的手动通知,首先检查值是否已更改来最大程度地减少发送不必要的通知。如下balance则可以实现仅在通知已更改时才提供通知。

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
上一篇 下一篇

猜你喜欢

热点阅读