KVO 和 KVC 的使用和實現

原文鏈接

瞭解 KVO/KVC

KVO/KVC 是觀察者模式在 Objective-C 中的實現,以非正式協議(Category)的形式被定義在 NSObject 中。從協議的角度看,是定義了一套讓開發者遵守的規範和使用的方法。在 Cocoa 的 MVC 框架中,架起 ViewController 和 Model 溝通的橋樑。

  1. // "NSKeyValueObserving.h"
  2. @interface NSObject(NSKeyValueObserving)
  3. // "NSKeyValueCoding.h"
  4. @interface NSObject(NSKeyValueCoding)

KVO 即 Key-Value-Observing,顧名思義用於觀察鍵值

  1. //通過此方法即可添加對象的觀察者
  2. - (void)addObserver:(NSObject *)observer
  3. forKeyPath:(NSString *)keyPath
  4. options:(NSKeyValueObservingOptions)options
  5. context:(void *)context;

KVC 即 Key-Value-Coding,用於鍵值編碼

  1. - (id)valueForKey:(NSString *)key;
  2. - (void)setValue:(id)value forKey:(NSString *)key;
  3. - (id)valueForKeyPath:(NSString *)keyPath;
  4. - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

如何使用 KVO/KVC

簡單使用

爲了讓各位童鞋更好的理解,下面主要使用例子講解 KVO/KVC 的使用

先定義一個 Persson 類,當做被觀察的對象

  1. //Person.h
  2. #import <Foundation/Foundation.h>
  3. @interface Person : NSObject
  4. @end
  1. // Person.m
  2. #import "Person.h"
  3. @interface Person () {
  4. NSString *address; //地址
  5. CGFloat weight; //體重
  6. }
  7. @property (nonatomic, copy) NSString *name; //名字
  8. @property (nonatomic, assign) NSInteger age; //年齡
  9. @end
  10. @implementation Person
  11. @end

再定義一個 PersonObserver 類,用於觀察 Person

  1. //PersonObserver.h
  2. #import <Foundation/Foundation.h>
  3. @interface PersonObserver : NSObject
  4. @end
  1. //PersonObserver.m
  2. #import "PersonObserver.h"
  3. @implementation PersonObserver
  4. //觀察者需要實現的方法
  5. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  6. {
  7. NSLog(@"old: %@", [change objectForKey:NSKeyValueChangeOldKey]);
  8. NSLog(@"old: %@", [change objectForKey:NSKeyValueChangeNewKey]);
  9. NSLog(@"context: %@", context);
  10. }
  11. @end

觀察者和被觀察者準備就緒,即可進行測試

  1. // 測試 KVO & KVC
  2. - (void)testMethod
  3. {
  4. Person *aPerson = [[Person alloc] init];
  5. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  6. //添加觀察者
  7. //也可以觀察 age、address、weight
  8. [aPerson addObserver:aPersonObserver
  9. forKeyPath:@"name"
  10. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  11. context:@"this is a context"];
  12. //設置key的value值,aPersonObserver接收到通知
  13. [aPerson setValue:@"LiLei" forKey:@"name"];
  14. NSLog(@"name: %@", [aPerson valueForKey:@"name"]);
  15. //移除觀察者
  16. [aPerson removeObserver:aPersonObserver forKeyPath:@"name"];
  17. }

當代碼執行到第16行時,aPersonObserver 接收到通知,打印 change 和 context 值。整個代碼的執行結果如下:

  1. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] old: <null>
  2. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] new: LiLei
  3. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] context: this is a context
  4. 2015-01-30 01:26:41.997 HelloWorld[4132:1588732] name: LiLei

如果 Person 類裏面還有個 Job 的屬性

  1. @property (nonautomic, strong) Job *aJob; //工作
  1. #import <Foundation/Foundation.h>
  2. @interface Job : NSObject
  3. @property (nonautomic, copy) NSString *companyName; //公司名字
  4. @property (nonautomic, assign) CGFloat salary; //薪水
  5. @end

要觀察和設置 Person 的薪水,只要這麼寫就可以

  1. // 觀察 salary
  2. [aPerson addObserver:aPersonObserver
  3. forKeyPath:@"aJob.salary"
  4. options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
  5. context:@"this is a context"];
  6. //設置月薪:20k
  7. [aPerson setValue:@"20000.0" forKey:@"aJob.salary"];

操作集合

如果 Person 需要車,就添加 cars 屬性,於是就有了很多車

  1. @property (nonautomic, copy) NSArray *cars; //很多車

對 NSArray *cars 這種有序集合屬性的操作有兩種方法

  • 使用 valueForKey 將 cars 直接取出來,再對集合元素進行操作
  • 通過下面的方法直接操作;
  1. /**
  2. * 有序集合的操作
  3. * 將所有方法 <Key> 替換成 Cars,且首字母大寫
  4. */
  5. //必須實現,對應於NSArray的基本方法count:
  6. -countOf<Key>
  7. //這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
  8. -objectIn<Key>AtIndex:
  9. -<Key>AtIndexes:
  10. //不是必須實現的,但實現後可以提高性能,其對應於 NSArray 方法 getObjects:range:
  11. -get<Key>:range:
  12. //兩個必須實現一個,類似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
  13. -insertObject:in<Key>AtIndex:
  14. -insert<Key>:atIndexes:
  15. //兩個必須實現一個,類似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
  16. -removeObjectFrom<Key>AtIndex:
  17. -remove<Key>AtIndexes:
  18. //可選的,如果在此類操作上有性能問題,就需要考慮實現之
  19. -replaceObjectIn<Key>AtIndex:withObject:
  20. -replace<Key>AtIndexes:with<Key>:

相對應的,像 NSSet 這種無序集合同樣也有如下的方法可以使用

  1. /**
  2. * 無序集合的操作
  3. * 將所有方法 <Key> 替換成 Cars,且首字母大寫
  4. */
  5. //必須實現,對應於NSArray的基本方法count:
  6. -countOf<Key>
  7. //這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
  8. -objectIn<Key>AtIndex:
  9. -<key>AtIndexes:
  10. //不是必須實現的,但實現後可以提高性能,其對應於 NSArray 方法 getObjects:range:
  11. -get<Key>:range:
  12. //兩個必須實現一個,類似 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
  13. -insertObject:in<Key>AtIndex:
  14. -insert<Key>:atIndexes:
  15. //兩個必須實現一個,類似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
  16. -removeObjectFrom<Key>AtIndex:
  17. -remove<Key>AtIndexes:
  18. //這兩個都是可選的,如果在此類操作上有性能問題,就需要考慮實現之
  19. -replaceObjectIn<Key>AtIndex:withObject:
  20. -replace<Key>AtIndexes:with<Key>:

但是,如果要使用這些方法,開發者需自己實現一遍,所以使用上相對麻煩。

鍵值驗證(KVV,Key-Value Validate)

KVC 提供了鍵值驗證(KVV)機制,讓開發者有機會能夠挽回錯誤,保證數據的一致性。開發者需先調用下面

  1. - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

這個方法會默認調用的實現方法如下

  1. - (BOOL)validate<Key>:error:

舉個例子,上面的例子中,希望能夠驗證 Person 的屬性 name 不爲空,就可以這麼寫

  1. // 測試 KVV
  2. - (void)testMethod
  3. {
  4. Person *aPerson = [[Person alloc] init];
  5. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  6. //添加觀察者
  7. [aPerson addObserver:aPersonObserver
  8. forKeyPath:@"name"
  9. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  10. context:@"this is a context"];
  11. NSString *name = @"LiLei";
  12. NSString *key = @"name";
  13. NSError *error = nil;
  14. // KVV 驗證
  15. BOOL isLegalName = [aPerson validateValue:&name forKey:key error:&error];
  16. if (isLegalName) {
  17. NSLog(@"it's a legal name.");
  18. [aPerson setValue:name forKey:key];
  19. }else{
  20. NSLog(@"the name is illegal.");
  21. }
  22. //移除觀察者
  23. [aPerson removeObserver:aPersonObserver forKeyPath:@"name"];
  24. }
  1. //Person.m
  2. //Person.m 文件新增驗證名字的KVV方法
  3. @implementation Person
  4. - (BOOL)validateName:(NSString **)name error:(NSError * __autoreleasing *)outError
  5. {
  6. if ((*name).length == 0)
  7. {
  8. (*name) = @"default name";
  9. return NO;
  10. }
  11. return YES;
  12. }
  13. @end

集合操作符(Collection Operator)

集合運算符是一種特殊的 key path,通過 - (id)valueForKeyPath:(NSString *)keyPath 方法獲取集合中的信息,其格式如下:

集合運算符(Collection Operator)

由數值組成的集合,總共有 5 中操作符

  • sum:集合中所有數值的和
  • avg:集合中所有數值的平均數
  • max:集合中的最大數值
  • min:集合中的最小數值
  • count:集合元素的數量

使用示例

  1. //假設在 Person 類中還有個存儲數值的數組 array
  2. //獲取array 中的所有數值的和
  3. CGFloat sum = [aPerson valueForKeyPath:@"@sum.array"];
  4. //獲取 array 中所有數值的平均數
  5. CGFloat avg = [aPerson valueForKeyPath:@"@avg.array"];

由對象組成的集合有 2 種操作符

  • unionOfObjects:返回集合中的所有元素
  • distinctUnionOfObjects:返回集合去重後的所有元素

由數組組成的集合(集合中有集合)有如下 3 種操作符。

  • unionOfArrays:用於Array集合,返回集合中的所有元素
  • distinctUnionOfArrays:用於Array集合,返回集合中去重後的所有元素
  • distinctUnionOfSets:用於Set集合,返回集合中(去重後)的所有元素

由於 Set 中的元素本身就是不重複的,所以沒有 unionOfSets 操作符。

手動鍵值觀察

通過自動屬性,建立鍵值觀察,都屬於自動鍵值觀察。因爲使用這種方法,只要設置鍵值,就會自動發出通知。而手動鍵值觀察,不能使用自動化屬性,需要自己寫 setter/getter 方法,手動發送通知。

  1. //手動通知的實現
  2. @interface Person : NSObject
  3. {
  4. NSString *name;
  5. }
  6. - (NSString *)name;
  7. - (void)setName:(int)theName;
  8. @end
  9. @implementation Person
  10. - (id) init
  11. {
  12. self = [super init];
  13. if (nil != self) {
  14. name = @"LiLei";
  15. }
  16. return self;
  17. }
  18. - (NSString *)name
  19. {
  20. return name;
  21. }
  22. - (void)setName:(NSString *)theName
  23. {
  24. //發送通知:鍵值即將改變
  25. [self willChangeValueForKey:@"name"];
  26. name = theName;
  27. //發送通知:鍵值已經修改
  28. [self didChangeValueForKey:@"name"];
  29. }
  30. /**
  31. * 當設置鍵值之後,通過此方法,決定是否發送通知
  32. */
  33. + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
  34. {
  35. //當 key 爲 name時,手動發送通知
  36. if ([key isEqualToString:@"age"]) {
  37. return NO;
  38. }
  39. //當爲其他key時,自動發送通知
  40. return [super automaticallyNotifiesObserversForKey:key];
  41. }
  42. @end

設置屬性之間的依賴

假如有個 Person 類,類裏有三個屬性,fullNamefirstNamelastName。按照之前的知識,如果需要觀察名字的變化,就要分別添加 fullNamefirstNamelastName 三次觀察,非常麻煩。如果能夠只觀察 fullName,並建立 fullName 和 firstNamelastName 的某種依賴關係,當發生變化時,也受到通知,那該多好啊!

KVC 剛好提供這種鍵之間的依賴方法,格式如下

  1. + (NSSet *)keyPathsForValuesAffecting<Key>;

這方法使得 Key 之間能夠建立依賴關係,爲了便於說明,直接用 屬性依賴 這個詞代替 Key 之間的依賴。含義不同,結果一致。

下面就使用這種方法解決 Key 之間的依賴關係。

Person 類爲被觀察者

  1. //Person.h
  2. #import <Foundation/Foundation.h>
  3. @interface Person : NSObject
  4. @end
  1. //Person.m
  2. #import "Person.h"
  3. @interface Person ()
  4. @property (nonatomic, copy) NSString *fullName; //名字,依賴於firstName、lastName
  5. @property (nonatomic, copy) NSString *firstName;
  6. @property (nonatomic, copy) NSString *lastName;
  7. @end
  8. @implementation Person
  9. //設置屬性依賴:fullName屬性依賴於firstName、lastName
  10. //如果觀察name,當firstName、lastName發生變化時,觀察者也會收到name變化通知
  11. + (NSSet *)keyPathsForValuesAffectingFullName
  12. {
  13. NSSet *set = [NSSet setWithObjects:@"firstName", @"lastName", nil];
  14. return set;
  15. }
  16. @end

PersonObserver 類爲觀察者

  1. //PersonObserver.h
  2. #import <Foundation/Foundation.h>
  3. @interface PersonObserver : NSObject
  4. @end
  1. //PersonObserver.m
  2. #import "PersonObserver.h"
  3. @implementation PersonObserver
  4. //觀察者需要實現的方法
  5. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  6. {
  7. NSLog(@"observer receive change infomation");
  8. }
  9. @end

準備就緒,就可以測試依賴的屬性了

  1. - (void)testMethod
  2. {
  3. Person *aPerson = [[Person alloc] init];
  4. PersonObserver *aPersonObserver = [[PersonObserver alloc] init];
  5. //觀察fullName屬性
  6. [aPerson addObserver:aPersonObserver
  7. forKeyPath:@"fullName"
  8. options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
  9. context:@"this is a context"];
  10. //設置firstName時,aPersonObserver仍然會收到name變化的通知
  11. [aPerson setValue:@"LiLei" forKey:@"firstName"];
  12. //移除觀察者
  13. [aPerson removeObserver:aPersonObserver forKeyPath:@"fullName"];
  14. }

輸出結果,發現雖然觀察的是 fullName,但是當修改 firstName 的時候,觀察者也會收到 fullName 變化的通知,達到了我們的期望。

  1. 2015-01-30 06:32:20.527 HelloWorld[28005:130136] observer receive change infomation

理解 KVO/KVC 的實現

KVO 是通過 isa-swizzling 實現的。

基本的流程就是編譯器自動爲被觀察對象創造一個派生類,並將被觀察對象的isa 指向這個派生類。如果用戶註冊了對某此目標對象的某一個屬性的觀察,那麼此派生類會重寫這個方法,並在其中添加進行通知的代碼。Objective-C 在發送消息的時候,會通過 isa 指針找到當前對象所屬的類對象。而類對象中保存着當前對象的實例方法,因此在向此對象發送消息時候,實際上是發送到了派生類對象的方法。由於編譯器對派生類的方法進行了 override,並添加了通知代碼,因此會向註冊的對象發送通知。注意派生類只重寫註冊了觀察者的屬性方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章