KVC/KVO 总结

2,196 阅读20分钟

KVC

Key-Value Coding基本原则

访问对象属性

@interface BankAccount: NSobject

@property (nonatomic) NSNumber *currentBalance; // An attribute
@property (nonatomic) Person *owner; // A to-one relation
@property (nonatomic) NSArray <Transaction *>*transactions; // A to-many relation

@end

currentBalance/owner/transactions都是BankAccount的属性。owner属性是一个对象,和BankAccount构成一对一的关系,owner对象中的属性改变后并不会影响到owner本身。

为了保持封装,对象通常为其接口上的属性提供访问器方法(accessor methods)。在使用访问器方法时必须在编译之前将属性名称写入代码中。访问器方法的名称成为使用它的代码的静态部分。例如: [myAccount setCurrentBalance:@(100.0)]; 这样缺乏灵活性,KVC提供了使用字符串标识符访问对象属性的更通用的机制。

使用key和key path 标识对象的属性

key: 标识特定属性的字符串。通常表示属性的key是代码中显示的属性本身的名称。 key必须使用ASCII编码,可能不包含空格,并且通常是以小写字母开头(URL除外)。 上面的赋值过程使用KVC表示: [myAccount setValue:@(100.0) forKey:@"currentBalance"];

key path: 用来指定要遍历的对象属性序列的一串使用“.”分隔的key。序列中的第一个键的属性是相对于接受者的,并且每个后续键是相对于前一个属性的值的。当需要使用一个方法来向下逐级获取对象层次结构时,key path特别有用。 例如,owner.address.street应用于银行账户实例的key path是指存储在银行账户所有者地址中的street字符串的值。

使用key获取属性值

- (void)getAttributeValuesUsingKeys {
    Account *myAccount = [[Account alloc] init];
    myAccount.currBalance = @100;
    
    Person *owner = [[Person alloc] init];
    Address *address = [[Address alloc] init];
    address.street = @"第三大道";
    owner.address = address;
    myAccount.owner = owner;
    
    Transaction *t1 = [[Transaction alloc] init];
    Person *p1 = [[Person alloc] init];
    p1.name = @"p1";
    t1.payee = p1;
    
    Transaction *t2 = [[Transaction alloc] init];
    Person *p2 = [[Person alloc] init];
    p2.name = @"p2";
    t2.payee = p2;
    
    NSArray *ts = @[t1, t2];
    myAccount.transactions = ts;
    
    NSNumber *currBalance = [myAccount valueForKey:@"currBalance"];
    NSLog(@"currBalance = %@", currBalance); // currBalance = 100
    
    NSString *street = [myAccount valueForKeyPath:@"owner.address.street"];
    NSLog(@"street = %@", street); // street = 第三大道
    
    NSDictionary *values = [myAccount dictionaryWithValuesForKeys:@[@"currBalance", @"owner"]];
    NSLog(@"values = %@", values); // values = {currBalance = 100; owner = "<Person: 0x60000179af40>";}
    
    NSArray *payees = [myAccount valueForKeyPath:@"transactions.payee.name"];
    NSLog(@"payees = %@", payees); // payees = (p1, p2)
    
    // Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Account 0x600002685ee0> valueForUndefinedKey:]'
    
    //    [myAccount valueForKey:@"owner.address.street"];
    //    [myAccount valueForKey:@"test"];
    //    [myAccount dictionaryWithValuesForKeys:@[@"currBalance", @"transactions.payee.name"]];
}

使用key设置属性值

- (void)settingAttributeValuesUsingKeys {
    Account *myAccount = [[Account alloc] init];
    [myAccount setValue:@100.0 forKey:@"currBalance"];
    NSLog(@"currBalance = %@", myAccount.currBalance); // currBalance = 100
    
    // operationTimes是非引用类型,这里进行了和NSNumber的自动转换
    [myAccount setValue:@10 forKey:@"operationTimes"];
    NSLog(@"operationTimes = %ld", myAccount.operationTimes); // operationTimes = 10
    
    Person *owner = [[Person alloc] init];
    Address *address = [[Address alloc] init];
   
    [myAccount setValue:address forKeyPath:@"owner.address"]; // 这时候owner还是null
    NSLog(@"address = %@", myAccount.owner.address); // address = (null)
    
    [myAccount setValue:owner forKeyPath:@"owner"];
    [myAccount setValue:address forKeyPath:@"owner.address"];
    NSLog(@"address = %@", myAccount.owner.address); // address = <Address: 0x600001a43550>
    
    [myAccount setValuesForKeysWithDictionary:@{@"currBalance": @200.0, @"owner": owner}];
    NSLog(@"currBalance = %@, owner = %@", myAccount.currBalance, myAccount.owner); // currBalance = 200, owner = <Person: 0x600001478ee0>
    
    // Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Account 0x6000029c2490> setValue:forUndefinedKey:]: xxx'
    //    [myAccount setValue:@"value" forUndefinedKey:@"undefinedKey"];
    //    [myAccount setValuesForKeysWithDictionary:@{@"currBalance": @200.0, @"owner.address.street": @"第一大道"}];
}

访问集合属性

符合键值编码的对象以与公开其他属性相同的方式公开其多对多属性。您可以使用valueForKey:setValue:forKey:来获取或设置集合属性。但是,当你想要操作这些集合内容的时候,使用协议定义的可变代理方法通常是最有效的。 该协议为集合对象访问定义了三种不同的代理方法,每种方法都有一个key和key path变量:

  • mutableArrayValueForKey:mutableArrayValueForKeyPath: 返回一个行为类似NSMutableArray的代理对象
  • mutableSetValueForKey:mutableSetValueFOrKeyPath: 返回一个行为类似NSMutableSet的代理对象
  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath: 返回一个行为类似NSMutableOrderedSet的代理对象 当您对代理对象进行操作,向对象添加元素,从中删除元素或者替换其中的元素时,协议的默认实现会相应地修改基础属性。这比使用valueForKey:获取一个不可变的集合对象,再创建一个可修改的集合,然后把修改后的集合通过setValue:forKey:更有效。在许多情况下,它比直接使用可变属性也是更有效的。这些方法为持有集合对象的对象们提供了维护KVO特性的好处。
- (void)accessingCollectionProperties {
    Transaction *t1 = [[Transaction alloc] init];
    Transaction *t2 = [[Transaction alloc] init];
    Account *myAccount = [[Account alloc] init];
    
    [myAccount addObserver:self forKeyPath:@"transactions" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    
    [myAccount setValue:@[t1, t2] forKey:@"transactions"];
    NSLog(@"1st transactions = %@", myAccount.transactions); // 1st transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>")
    NSMutableArray <Transaction *>*transactions = [myAccount mutableArrayValueForKey:@"transactions"];
    
    [transactions addObject:[[Transaction alloc] init]];
    NSLog(@"2nd transactions = %@", myAccount.transactions); // 2nd transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420>","<Transaction: 0x6000009cabf0>")
    
    [transactions removeLastObject];
    NSLog(@"3th transactions = %@", myAccount.transactions); // 3th transactions = ("<Transaction: 0x6000009d1400>","<Transaction: 0x6000009d1420")
}

使用集合操作符

当您向valueForKeyPath:消息发送符合键值编码的对象时,可以在key path中嵌入集合运算符。集合运算符是一个小的关键字列表之一,前面是一个@符号,它指定了getter应该执行的操作,以便在返回之前以某种方式操作数据。NSObjectvalueForKeyPath:提供了默认实现。 当key path包含集合运算符时,运算符之前的部分称为左键路径,指示相对于消息接受者操作的集合,当你直接向一个集合(例如NSArray)发送消息时左键路径或许可以省略。操作符之后的部分称为右键路径,指定操作符应处理的集合中的属性,除了@count之外的所有操作符都需要一个右键路径。

集合运算符表现出三种基本类型的行为:

  • 聚合运算符以某种方式合并集合的对象,并返回通常与右键路径中指定的属性的数据类型匹配的单个对象。@count是一个例外,它没有正确的关键路径并始终将返回一个NSNumber实例。包括:@avg/@count/@max/@min/@sum
  • 数组运算符返回一个NSArray实例,该实例包含命名集合中保存的对象的某个子集。包含:@distinctUnionOfObjects/@unionOfObjects
  • 嵌套运算符处理包含其他集合的集合,并根据操作符返回一个NSArrayNSSet实例,它以某种方式组合嵌套集合的对象。包含:@distinctUnionOfArrays/@unionOfArrays/@distinctUnionOfSets

示例

- (void)usingCollectionOperators {
    Transaction *t1 = [Transaction transactionWithPayee:@"Green Power" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:100]];
    Transaction *t3 = [Transaction transactionWithPayee:@"Green Power" amount:@(170.00) date:[NSDate dateWithTimeIntervalSinceNow:300]];
    Transaction *t5 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];
    Transaction *t6 = [Transaction transactionWithPayee:@"Car Loan" amount:@(250.00) date:[NSDate dateWithTimeIntervalSinceNow:600]];
    Transaction *t13 = [Transaction transactionWithPayee:@"Animal Hospital" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:500]];
    
    NSArray *transactions = @[t1, t3, t5, t6, t13];
    
    /* 聚合运算符
     * 聚合运算符可以处理数组或属性集,从而生成反映集合某些方面的单个值。
     */
    // @avg 平均值
    NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
    NSLog(@"transactionAverage = %@", transactionAverage); // transactionAverage = 278
    // @count 个数
    NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
    NSLog(@"numberOfTransactions = %@", numberOfTransactions); // numberOfTransactions = 5
    // @max 最大值 使用compare:进行比较
    NSDate *latestDate = [transactions valueForKeyPath:@"@max.date"];
    NSLog(@"latestDate = %@", latestDate); // latestDate = Thu Nov  1 15:05:59 2018
    // @min 最小值 使用compare:进行比较
    NSDate *earliestDate = [transactions valueForKeyPath:@"@min.date"];
    NSLog(@"earliestDate = %@", earliestDate);// earliestDate = Thu Nov  1 14:57:39 2018
    // @sum 总和
    NSNumber *amountSum = [transactions valueForKeyPath:@"@sum.amount"];
    NSLog(@"amountSum = %@", amountSum); // amountSum = 1390
    
    /* 数组运算符
     *
     * 数组运算符导致valueForKeyPath:返回与右键路径指示的特定对象集相对应的对象数组。
     * 如果使用数组运算符时任何叶对象为nil,则valueForKeyPath:方法会引发异常。
     **/
    // @distinctUnionOfObjects 创建并返回一个数组,该数组包含与右键路径指定的属性对应的集合的不同对象。会删除重复对象。
    NSArray *distinctPayees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
    NSLog(@"distinctPayees = %@", distinctPayees); // distinctPayees = ("Green Power", "Animal Hospital", "Car Loan")
    
    // @unionOfObjects 创建并返回一个数组,该数组包含与右键路径指定的属性对应的集合的所有对象。不删除重复对象
    NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
    NSLog(@"payees = %@", payees); // payees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital")
    
    /** 嵌套运算符
     *
     * 嵌套运算符对嵌套集合进行操作,集合中的每个条目都包含一个集合。
     * 如果使用数组运算符时任何叶对象为nil,则valueForKeyPath:方法会引发异常。
     **/
    Transaction *moreT1 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(120.00) date:[NSDate dateWithTimeIntervalSinceNow:10]];
    Transaction *moreT2 = [Transaction transactionWithPayee:@"General Cable - Cottage" amount:@(1550.00) date:[NSDate dateWithTimeIntervalSinceNow:3]];
    Transaction *moreT7 = [Transaction transactionWithPayee:@"Hobby Shop" amount:@(600.00) date:[NSDate dateWithTimeIntervalSinceNow:160]];
    NSArray *moreTransactions = @[moreT1, moreT2, moreT7];
    NSArray *arrayOfArrays = @[transactions, moreTransactions];
    // @distinctUnionOfArrays  指定@distinctUnionOfArrays运算符时,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定的属性对应的所有集合的组合的不同对象。
    NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
    NSLog(@"collectedDistinctPayees = %@", collectedDistinctPayees); // collectedDistinctPayees = ( "General Cable - Cottage", "Animal Hospital", "Hobby Shop", "Green Power", "Car Loan")
    // @unionOfArrays 与@distinctUnionOfArrays 不同的是不会删除相同的元素
    NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
    NSLog(@"collectedPayees = %@", collectedPayees); // collectedPayees = ("Green Power", "Green Power", "Car Loan", "Car Loan", "Animal Hospital", "General Cable - Cottage", "General Cable - Cottage", "Hobby Shop")
    
    // @distinctUnionOfSets 与@distinctUnionOfArrays作用相同,只是用于NSSet对象而不是NSArray
}

访问者搜索模式

NSObject提供的NSkeyValueCoding协议的默认实现使用明确定义的规则集将基于键的访问器调用映射到对象的基础属性。这些协议方法使用“key”在其自己的对象实例中搜索访问器、实例变量以及遵循某个命名规则的相关方法。虽然您很少修改此默认搜索,但了解它的工作方式会有所帮助,既可以跟踪键值编码对象的行为,也可以使您自己的对象兼容KVC。

Getter的搜索模式

valueForKey:的默认实现是,给定key参数作为输入,通过下面的过程,在接收valueForKey:调用的类实例中操作。

  1. 按顺序搜索访问器方法get<Key>/<key>/is<Key>/_<key>。如果找到,调用该方法并且带着方法的调用结果调转到第5步执行;否则,继续下一步。
  2. 如果没有找到简单的访问方法,搜索其名称匹配某些模式的方法的实例。其中匹配模式包含countOf<Key>objectIn<Key>AtIndex:(对应于NSArray定义的基本方法),和<key>AtIndexs:(对应于NSArray的方法objectsAtIndexs:) 一旦找到第一个和其他两个中的至少一个,则创建一个响应所以NSArray方法并返回该方法的集合代理对象。否则,执行第3步。 代理对象随后将任何NSArray接收到的一些组合的消息。**实际上,与符合键值编码对象一起工作的代理对象允许底层属性的行为就像它是NSArray一样,即便它不是。
  3. 如果没有找到简单的访问器方法或数组访问方法组,则寻找三个方法countOf<Key>/enumeratorOf<Key>/memberOf<Key>:,对应NSSet类的基本方法。 如果三个方法全找到了,则创建一个集合代理对象来响应所有的NSSet方法并返回。否则,执行第4步。
  4. 如果上面的方法都没有找到,并且接受者的类方法accessInstanceVariablesDirectly返回YES(默认YES),则按序搜索以下实例变量:_<key>/_is<Key>/<key>/is<Key>。如果找到其中之一,直接获取实例变量的值并跳转到第5步;否则执行第6步。
  5. 如果检索到的属性值是对象指针,则只返回结果;如果值是受NSNumber支持的标量,则将其存储在NSNumber实例中并返回;如果结果是NSNumber不支持的标量,则转换成NSValue对象并返回
  6. 如果以上所有的尝试都失败了,则调用valueForUndefinedKey:,这个方法默认抛出异常,NSObject的子类可以重写来自定义行为。

Setter的搜索模式

setValue:forKey:的默认实现是给定keyvalue作为参数输入,尝试把value设置给以key命名的属性。过程如下:

  1. 按序搜索set<Key>:_set<Key>,如果找到,则使用输入参数调用并结束。
  2. 如果没有找到简单的访问器方法,并且如果类方法accessInstanceVariablesDirectly返回YES(默认为YES),则按序搜索以下实例变量: _<key>/_is<Key>/<key>/is<Key>,如果找到了则直接进行赋值并结束。
  3. 以上方法皆失败则调用setValue:forUndefinedKey:,这个方法默认抛出异常,NSObject的子类可以自定义。

KVO

Key-value observing提供了一种机制,允许对象把自身属性的更改通知给其他属性。它对应用程序中model和controller层之间的通信特别有用。通常,控制器对象观察模型对象的属性,视图对象通过控制器观察模型对象的属性。另外,一个模型对象或许会观察另一个模型对象(通常用与确认从属值何时改变)或甚至自身(再次确认从属值何时改变)。 你可以观察属性,包括简单属性,一对一关系和多对多关系。多对多关系的观察者被告知所作出的改变的类型——以及改变中涉及哪些对象。

注册KVO

  • 使用addObserver:forKeyPath:options:content:方法来给observer注册一个observed object
  • 在observer内部实现observerValueForKeyPath:ofObject:change:context:来接收更改的通知消息。
  • 当不再应该接收消息时,使用removeObserver:forKeyPath:方法来反注册观察者。起码也要在observer被移除前调用这个方法。

注册Observer

addObserver:forKeyPath:options:content:

options

options参数指定了一个按位OR的常量选项,会影响通知中提供的更改字典的内容和生成通知的方式。 你可以选择使用NSKeyValueObservingOptionOld选项,在被观察的属性修改前收到旧值;也可以使用NSKeyValueObservingOptionNew来获取修改后的新值。通过NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew获取两者。 使用NSKeyValueObservingOptionInitial选项,让被观察的属性在addObserver:forKeyPath:options:context方法返回前发送即时通知。你可以使用此附加的一次性通知来在观察者中建立属性的初始值。 通过包含NSKeyValueObservingOptionPrior来指示被观察对象在属性更改之前发送通知(除了在更改之后发送通知)。在更改之前发送的通知中的change字典始终包含NSKeyValueChangeNotificationIsPriorKey,其值是包含布尔值YES的NSNumber对象,但不包含NSKeyValueChangeNewKey的内容。如果指定此选项,则更改后发送的通知中的change字典的内容和未指定此选项时包含的内容相同。当观察者自己的键值观察兼容性要求它为自己的一个属性调用-willChangexxx方法之一时,可以使用此选项,并且该属性的值取决于被观察对象的属性的值。

context

addObserver:forKeyPath:options:context:消息中的上下文指针包含将在相应的更改通知中传递回观察者的任意数据。您可以使用NULL来完全指定并依赖于key path字符串来确定更改通知的来源,但是这种方法可能会导致其超类也因不同原因观察到相同密钥路径的对象出现问题。

一个更安全且具有扩展性的方法是使用content来确保你收到的通知就是发给你的而不是超类的。

类中唯一命名的静态(static)变量的地址是一个很好的content。在超类或子类中以类似的方式选择的上下文不太可能重叠。您可以为整个类选择同一个上下文,并根据通知消息中的key path字符串来确定更改的内容;或者,您可以为每个观察到的密钥路径创建不同的上下文,从而完全绕过字符串比较的需要,从而实现更有效的通知解析。

- (void)registerAsObserver {
    BankAccount *myAccount = [[BankAccount alloc] init];
    [myAccount addObserver:self forKeyPath:@"currBalance" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:PersonAccountBalanceContext];
    myAccount.currBalance = @100;
}

注意,键值观察addObserver:forKeyPath:options:context:方法不对观察者、被观察的对象、上下文保持强引用。如需要,你应该对它们保持强引用。

接受改变的通知

当对象的被观察属性值改变的时候,观察者对象会收到observeValueForKeyPath:ofObject:change:context:消息。所有的观察者必须实现这个方法。

观察对象提供触发通知的key path,自身作为objectchange字典包含改变的细节,并且context指针就是观察者被注册时提供的。

NSKeyValueChangeKindKey提供改变类型的信息。NSKeyValueChangeKindKey表示观察对象的值已更改。如果观察的属性是一个对多的关系,NSKeyValueChangeInsertion/NSKeyValueChangeRemoval/NSKeyValueChangeReplacement分别表示集合的插入、删除、替换操作。NSKeyValueChangeIndexesKey表示集合中已更改内容的NSIndexSet

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonAccountBalanceContext) {
        NSLog(@"PersonAccountBalanceContext 对应的属性改变了");
    } else if (context == PersonAccountTransactionContext) {
        if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeSetting) {
            NSLog(@"集合内容赋值 索引为:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeInsertion) {
            NSLog(@"集合内容插入 索引为:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeRemoval) {
            NSLog(@"集合内容删除 索引为:%@", change[NSKeyValueChangeIndexesKey]);
        } else if ([change[NSKeyValueChangeKindKey] unsignedIntValue] == NSKeyValueChangeReplacement) {
            NSLog(@"集合内容替换 索引为:%@", change[NSKeyValueChangeIndexesKey]);
        }
    }
}

移除观察者对象

通过想观察者发送removeObserver:forKeyPath:context消息来移除观察者对象。收到该消息后,观察者对象将不再接收任何observerValueForKeyPath:ofObject:change:context中指定key path/object的消息。

删除观察者时,注意:

  • 移除和添加的方法要保持对称,否则会引发异常。如果无法保持对称,则把移除的方法放到try/catch块中。
  • 对象释放时,不会自动把自己从观察者中移除,此时被观察者继续发送通知。但是就像任何其他消息一样,改变的通知发送给了已经释放的对象会触发内存访问异常。因此,务必在观察者从内存中消失前,将其移除
  • 协议没有提供方法来查询一个对象是否是观察者或被观察者。你必须在代码中自行避免错误。典型的方案是在观察者初始化期间(init或dealloc)注册为观察者,并在释放时(dealloc)注销。

兼容KVO

为了让特定属性符合KVO标准,class必须满足一下内容:

  • 该类必须是符合该属性的KVC
  • 该类会为该属性触发KVO通知
  • 相关的key已经被成功注册

有两种技术可确保发出KVO通知。NSObject提供自动支持,默认情况下可用于符合键值编码的类的所有属性。通常,如果你遵守Cocoa编码和命名约定,则可以使用自动通知,而不必编写任何代码。

手动方式为通知触发时提供了更多的控制权,并且需要额外编码。你可以通过实现automaticallyNotifiesObserversForKey:来控制子类属性的自动通知。

自动通知

下列方法列举了会触发自动通知的一些场景:

//调用访问器方法。
[account setName:@“Savings”];
 
//使用setValue:forKey:。
[account setValue:@“Savings”forKey:@“name”];
 
//使用密钥路径,其中'account'是'document'的kvc兼容属性。
[document setValue:@“Savings”forKeyPath:@“account.name”];
 
//使用mutableArrayValueForKey:检索关系代理对象。
Transaction * newTransaction = <#为帐户#>创建新交易;
NSMutableArray * transactions = [account mutableArrayValueForKey:@“transactions”];
[transactions addObject:newTransaction];

手动通知

有些情况下,你可能想要控制通知的过程,例如,最大限度减少因应用程序特定原因而不必要的触发通知,或把一组通知整合到一个。

手动通知和自动通知不是互斥的。手动和自动的通知可以同时触发。如果你只想要手动触发,则需要通过重写automaticallyNotifiesObserversForKey:方法来禁止自动通知。

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

**要实现手动观察者通知,你要在值改变前调用willChangeValueForKey:,并在值改变后调用didChangeValueForKey:。有三组类似的方法:

  • willChangeValueForKey:didChangeValueForKey:。用于单个对象
  • willChange:valuesAtIndexes:forKey:didChange:valuesAtIndexes:forKey:。用于有序集合
  • willChangeValueForKey:withSetMutation:usingObjects:willChangeValueForKey:withSetMutation:usingObjects:。用于无须集合

下面在访问器方法中手动触发:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}

为了减少不必要的通知,可以先检查值是否改变了,然后决定是否发通知:

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

如果一个操作导致多个key发生改变,必须嵌套发送通知:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}

在有序的to-many关系中,除了指定更改的key,还不许指定更改的类型和所涉及对象的索引。

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

注册从属keys

在许多情况下,一个属性的值取决于另一个对象中的一个或多个其他属性的值。如果一个属性的值发生更改,则还应标记派生属性的值以进行更改。

To-One 关系

要为一对一关系自动触发通知,应该重写keyPathsForValuesAffectingValueForKey或实现一个合适的方法,该方法遵循它为注册依赖键定义的模式。

例如,fullName取决于firstNamelastName。返回fullName的方法可以写成如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@“%@%@”,firstName,lastName];
}

firstNamelastName发生改变时,必须通知观察fullName属性的程序,因为它们影响这个属性的值。

一个解决方案是重写keyPathsForValuesAffectingValueForKey来指定fullName属性依赖于lastNamefirstName

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

重写通常应该调用super并返回一个集合,以免影响超类中对此方法的重写。

通过重写keyPathsForValuesAffecting<Key>也可以达到相同的效果。

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

To-many 关系

keyPathsForValuesAffectingValueForKey:方法不支持to-many关系的key paths。可以使用下面两种方案来处理to-many 关系:

  1. 使用key-value observing注册父项作为子项的相关属性观察者。当子对象添加到关系或从关系中删除的时候,你必须添加或删除父对象。在observeValueForKeyPath:ofObject:change:context:方法中,你可以更新依赖值以相应更改,如下:
[self addObserver:self forKeyPath:@"transactions" options:NSKeyValueObservingOptionNew context:NULL];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"amount = %@", [self valueForKeyPath:@"transactions.@sum.amount"]);
    [self setTotalConsumption:[self valueForKeyPath:@"transactions.@sum.amount"]];
}
  1. 在Core Data中,则可以将父项作为其托管对象上下文的观察者注册到应用程序的通知中心。父项应以类似于键值观察的方式回应孩子们发布的相关变更通知。

Key-Value Observing 的实现细节

自动key-value observing 是使用一种叫做isa-swizzling的技术实现的。

isa指针指向维护一个调度表(dispatch table)的对象的类。该调度表包含了指向该类实现的方法的指针,以及其他数据。

当观察者注册对象的属性时,观察对象的isa指针被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。

永远不要依赖isa指针来确定类成员。而应该使用class方法来决定实例所属的类。

参考连接

示例代码

KVC

KVO