Objective-C(九)KVC與KVO

本文是Objective-C系列的第9篇,主要講述了KVO的底層實現,以及KVC的使用及KVC中調用流程。html

1、概述

KVO全稱Key Value Observing,是蘋果提供的一套事件通知機制。容許對象監聽另外一個對象特定屬性的改變,並在改變時接收到事件。因爲KVO的實現機制,只針對屬性纔會發生做用,通常繼承自NSObject的對象都默認支持KVOgit

KVONSNotificationCenter都是iOS中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係,KVO是一對一的,而不是一對多的。KVO對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽。github

KVO能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。經過KVCmutableArrayValueForKey:等方法得到代理對象,當代理對象的內部對象發生改變時,會回調KVO監聽的方法。集合對象包含NSArrayNSSetapi

2、KVO基本使用

項目代碼KVO-01-usage數組

2.1 註冊觀察者

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[BFPerson alloc] init];
    self.person1.age = 28;
    self.person1.name = @"weng";
    [self addObserver];
}
- (void)addObserver
{
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self forKeyPath:@"name" options:option context:@"name change"];
}
複製代碼

2.2 監聽回調

/** 觀察者監聽的回調方法 @param keyPath 監聽的keyPath @param object 監聽的對象 @param change 更改的字段內容 @param context 註冊時傳入的地址值 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
複製代碼

2.3 調用

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}
複製代碼

2.3.1 其餘調用方式

//下面調用方式均可以出發KVO
    self.person1.age = 29;
    [self.person1 setAge:29];
    [self.person1 setValue:@(29) forKey:@"age"];
    [self.person1 setValue:@(29) forKeyPath:@"age"];
複製代碼

2.3.2 手動調用

KVO在屬性發生改變時的調用是自動的,若是想要手動控制這個調用時機,或想本身實現KVO屬性的調用,則能夠經過KVO提供的方法進行調用。bash

下面以age屬性爲例:app

2.3.2.1 禁用自動調用

//age不須要自動調用,age屬性以外的(含name)自動調用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    BOOL automatic = NO;
    if ([key isEqualToString:@"age"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automatic;
}
複製代碼

上面方法也等同於下面兩個方法:ide

+ (BOOL)automaticallyNotifiesObserversOfAge
{
    return NO;
}
複製代碼

針對每一個屬性,KVO都會生成一個**‘+ (BOOL)automaticallyNotifiesObserversOfXXX’**方法,返回是否能夠自動調用KVO函數

假如實現上述方法,咱們會發現,此時改變age屬性的值,沒法觸發KVO,還須要實現手動調用才能觸發KVO。工具

2.3.2.2 手動調用實現

- (void)setAge:(NSInteger)age
{
    if (_age != age) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }
}
複製代碼

實現了(1)禁用自動調用(2)手動調用實現 兩步,age屬性手動調用就實現了,此時能和自動調用同樣,觸發KVO。

2.4 移除觀察者

- (void)dealloc
{
    [self removeObserver];
}

- (void)removeObserver
{
    [self.person1 removeObserver:self forKeyPath:@"age"];
    [self.person1 removeObserver:self forKeyPath:@"name"];
}
複製代碼

2.5 Crash

KVO若使用不當,極容易引起Crash。相關試驗代碼在KVO-02-crash

2.5.1 觀察者未實現監聽方法

若觀察者對象**-observeValueForKeyPath:ofObject:change:context:**未實現,將會Crash

Crash:Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.

2.5.2 未及時移除觀察者

Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)

//觀察者ObserverPersonChage
@interface ObserverPersonChage : NSObject
  //實現observeValueForKeyPath: ofObject: change: context:
@end

//ViewController
- (void)addObserver
{
    self.observerPersonChange = [[ObserverPersonChage alloc] init];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
    [self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}

//點擊按鈕將觀察者置爲nil,即銷燬
- (IBAction)clearObserverPersonChange:(id)sender {
    self.observerPersonChange = nil;
}

//點擊改變person1屬性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}
複製代碼
  1. 假如在當前ViewController中,註冊了觀察者,點擊屏幕,改變被觀察對象person1的屬性值。

  2. 點擊對應按鈕,銷燬觀察者,此時self.observerPersonChange爲nil。

  3. 再次點擊屏幕,此時Crash;

2.5.3 屢次移除觀察者

Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path "age" from <BFPerson 0x6000014acd00> because it is not registered as an observer.

2.6 keyPath字符串的弊端

在註冊Observe時,傳入keyPath爲字符串類型,keyPath極容易誤寫。

[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
複製代碼

優化的方案是:

[self.person1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:option context:@"age change"];
複製代碼

2.7 屬性依賴

/** 若是age改變 觀察者也會收到name改變的通知 */
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAge
{
    NSSet *set = [NSSet setWithObjects:@"name", nil];
    return set;
}
複製代碼

3、原理

1. 發現中間對象

爲了區分在添加KVO以後,對象以及對應的屬性設值方法發生的變化,咱們進行了以下測試:

image-20181126172954638

觀察方法實現:

image-20181126173559094

  • 添加KVO先後,person1指向的類對象元類對象,以及setAge:均發生了變化;
  • 添加KVO後,person1中的isa指向了NSKVONotifying_BFPerson類對象;
  • 添加KVO以後,setAge:的實現調用的是:Foundation 中 _NSSetLongLongValueAndNotify方法;

2. 探索調用流程

重寫項目在KVO-03-princlipe

(1)重寫BFPerson的下列方法

  • - setAge:
  • -willChangeValueForKey:
  • -didChangeValueForKey:

(2)調試

咱們經過重寫方法後,進行打印測試

image-20181126230732045

結合

  • 重寫打印後的日誌:

BFPerson willChangeValueForKey: - begin

BFPerson willChangeValueForKey: - end

BFPerson setAge: begin

BFPerson setAge: end

BFPerson didChangeValueForKey: - begin

-[ViewController observeValueForKeyPath:ofObject:change:context:]---監聽到BFPerson的age屬性值改變了 - {

kind = 1;

new = 29;

old = 28;

} - age chage

BFPerson didChangeValueForKey: - end

  • 彙編調用棧

image-20181126231110178

  • 中間對象——NSKVONotifying_BFPerson

咱們整理出一個完整的方法鏈:

  1. self.person1.age = 29;
  2. Foundation _NSSetLongLongValueAndNotify
    1. willChangeValueForKey:
    2. [BFPerson segAge:]
    3. didChangeValueForKey:
      1. -[ViewController observeValueForKeyPath:ofObject:change:context:]

以下圖:

image-20181127090036039

可是這些不足以反應真正完整的KVO實現。

3.重現KVO

下面是摘自官方文檔給出的原理描述:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

咱們同時觀察添加KVO以後,中間對象的方法列表以及未添加以前的方法列表:

方法列表(忽略name屬性相關方法)
BFPerson test, .cxx_destruct, setAge:, age
NSKVONotify_BFPerson setAge:, class, dealloc, _isKVOA
  • isa交換技術

    • 交換以後,調用任何BFPerson對象的方法,都會通過NSKVONotify_BFPerson,可是不一樣的方法,有不一樣的處理方式。

      • 調用監聽的屬性設置方法,如 setAge:,都會先調用NSKVONotify_BFPerson對應的屬性設置方法;
      • 調用非監聽屬性設置方法,如test,會經過NSKVONotify_BFPersonsuperclass,找到BFPerson類對象,再調用其[BFPerson test]方法
    • 交換以後,isa指向的並非該類的真實反映,一樣object_getClass返回的是isa指向的對象,因此也是不可靠的。

      好比使用KVO以後,經過object_getClass獲得的是生成的中間對象NSKVONotify_BFPerson,而不是BFPerson

    • 要想得到該類真實的對象,須要經過class對象方法獲取。

      假如經過**[self.person1 class]**獲得的是BFPerson對象。

  • **[self.person1 class]**獲得的仍然是BFPerson對象,爲何?

    • NSKVONotify_BFPerson重寫了其class對象方法,返回的是BFPerson
  • _isKVOA

    返回是不是KVO;

  • delloc

    作一些清理工做

到此,基本上NSKVONotifying_BFPerson類已經成型(相關代碼參考項目),結合調用流程,咱們繪製出下面對比圖。

(1)未使用KVO對象

image-20181127084920073

(2)使用KVO——生成中間對象

image-20181127084939046

(3)使用KVO——執行流

image-20181127084950233

4、KVC基本使用

項目源碼在:KVC-01-usage

(1)常見的API

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 
複製代碼

其中,有兩個方法要注意:

  • valueForKey與objectForKey的區別
valueForKey objectForKey
無key的處理 無該key,crash,NSUndefinedKeyException 無該key返回nil
來源 KVC主要方法 NSDictionary的方法
符號 若以 @ 開頭,去掉 @ ,用剩下部分做爲 key 執行 [super valueForKey:] key 不是以 @ 符號開頭, 二者等同
  • setValue與setObject的區別
setValue setObject
value value可爲nil,當value爲nil的時候,會自動調用removeObject:方法 value是不能爲nil
來源 KVC的主要方法 NSMutabledictionary特有的
key的參數 只能是NSString setObject: 可任何類型

NSKeyValueCoding類別中還有其餘的一些方法,例如

//默認返回YES,表示若是沒有找到set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供屬性值確認的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一個方法同樣,只不過是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//若是你在SetValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;

//輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
複製代碼

(2)集合API

有序集合對應方法以下:

-countOf<Key>//必須實現,對應於NSArray的基本方法count:2 
    
-objectIn<Key>AtIndex:
-<key>AtIndexes://這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現的,但實現後能夠提升性能,其對應於 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://可選的,若是在此類操做上有性能問題,就須要考慮實現之
複製代碼

無序集合對應方法以下:

-countOf<Key>//必須實現,對應於NSArray的基本方法count:

-objectIn<Key>AtIndex:
-<key>AtIndexes://這兩個必須實現一個,對應於 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必須實現的,但實現後能夠提升性能,其對應於 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://兩個必須實現一個,相似於 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://這兩個都是可選的,若是在此類操做上有性能問題,就須要考慮實現之
複製代碼

(3)使用場景

a.動態地取值和設值

b.訪問和修改私有變量

c.Model和字典轉換

d.修改一些控件的內部屬性

例如設置:UITextField中的placeHolderText

[textField setValue:[UIFont systemFontOfSize:25.0] forKeyPath:@"_placeholderLabel.font"];
複製代碼

如何獲取控件的內部屬性?

unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
for (int i = 0; i < count; i++) {
    objc_property_t property = properties[i];
    const char *name = property_getName(property);
    NSLog(@"name:%s",name);
}
複製代碼

e.高階消息傳遞

當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每個對象,而不是容器自己進行操做。結果會被添加進返回的容器中,這樣,開發者能夠很方便的操做集合來返回另外一個集合。

NSArray *arr = @[@"ali",@"bob",@"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str  in arrCap) {
    NSLog(@"%@",str);        //Ali\Bob\Cydia
}
複製代碼

f.KVC中的函數操做集合

  • 簡單集合運算符

    • @avg
    • @count
    • @max
    • @min
    • @sum
    @interface Book : NSObject
    @property (nonatomic,assign)  CGFloat price;
    @end
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    複製代碼
  • 對象運算符

    • @distinctUnionOfObjects
    • @unionOfObjects
    // 獲取全部Book的price組成的數組,而且去重
    NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    複製代碼
  • Array和Set操做符(集合中包含集合的情形)

    • @distinctUnionOfArrays
    • @unionOfArrays
    • @distinctUnionOfSets

    collection_keypath

5、KVC原理

項目源碼在**KVC-02-principle**

  • setValue:forKey

    image-20181127173412899

  • valueForKey

    image-20181127174325478

6、擴展

1._NSSetLongLongValueAndNotify

//抽出Foundation庫,查看其中Notify的函數
$ nm ./Foundation | grep LongValueAndNotify                                       
22bc9290 t __NSSetLongLongValueAndNotify
22bc90a0 t __NSSetLongValueAndNotify
22bc93a0 t __NSSetUnsignedLongLongValueAndNotify
22bc9198 t __NSSetUnsignedLongValueAndNotify
22bc9d18 t ____NSSetLongLongValueAndNotify_block_invoke
22bc9ca8 t ____NSSetLongValueAndNotify_block_invoke
22bc9d4c t ____NSSetUnsignedLongLongValueAndNotify_block_invoke
22bc9ce0 t ____NSSetUnsignedLongValueAndNotify_block_invoke
複製代碼

參考

連接

  1. Key-Value Coding Programming Guide
  2. Key-Value Observing Programming Guide
  3. KVC 和 KVO
  4. Key-Value Observing

示例代碼

  1. KVC-01-usage
  2. KVC-02-principle
  3. KVO-01-usage
  4. KVO-02-crash
  5. KVO-03-principle
  6. KVOLearnDemo

工具

KVOController Facebook出品的KVO封裝庫。

相關文章
相關標籤/搜索