KVC、KVO 相關(guān)知識點

1.KVC

關(guān)于 KVCKVO ,我之前的總結(jié)文章有寫過,但是趨于表面,沒有探究其內(nèi)部真正的實現(xiàn)原理和進(jìn)階用法,這次總結(jié)正好給了我很好的學(xué)習(xí)機(jī)會,在此深入的總結(jié)一下 KVCKVO 。

KVC,即是指 NSKeyValueCoding,一個非正式的 Protocol,提供一種機(jī)制來間接訪問對象的屬性。KVO 就是基于 KVC 實現(xiàn)的關(guān)鍵技術(shù)之一,相關(guān)的技術(shù)還有 Cocoa 綁定,Core Data 和 AppleScript。

Api示例

Objective-CKVC 的定義是對 NSObject 的擴(kuò)展來實現(xiàn)的。所以對于所有繼承了 NSObject 在類型,都可以使用KVC ,下面是 KVC 最為重要的四個方法

- (nullable id)valueForKey:(NSString *)key;                          //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過KeyPath來設(shè)值

一般來講,Obj-C 對象中都會有一些屬性。如代碼所示

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** name */
@property ( nonatomic,copy ) NSString *name;
/** Address */
@property ( nonatomic,copy ) NSString *address;
/** Friends */
@property ( nonatomic,copy ) NSArray<Person *> *address;
/** Spouse */
@property ( nonatomic,copy ) Person *Spouse;

@end

上面的 Person 對象所擁有的多個屬性,以 KVC 的角度來看,就是 Person 對象的 name , address 等屬性分別有一個Value 對應(yīng)他們的 Key 值。

  • Key 是一個字符串類型。
  • Value 可以為任何類型。

KVC 為存取值提供了兩個最基礎(chǔ)的方法。

Person *man = [Person new];
// 存值
[man setValue:@"LiMing" forKey:@"name"];
// 取值
NSString *name = [man valueForKey:@"name"];

KVC 為了便于使用還提供了另外兩個方法。

假設(shè)我們之前創(chuàng)建的這個對象有一個配偶,配偶也是一個Person對象,此時我們想在man這里讀出womanname屬性

可以這樣操作

Person *woman = [Person new];
man.spouse = woman;
[man setValue:@"Lily" forKeyPath:@"spouse.name"];
NSLog(@"%@",[man valueForKeyPath:@"spouse.name"]);
//  Key 與 KeyPath 要區(qū)分開來
//  Key 可以讓你從一個對象中獲取值
//  KeyPath  可以讓你通過連續(xù)的多個Key獲取值,著多個key值用點號 “.” 分割連接起來

簡單對比一下

//  結(jié)果一樣的,但是用 KeyPath 更簡單
[man valueForKeyPath:@"spouse.name"]
[[man valueForKey:@"spouse"] valueForKey:@"name"];

// 其實點語法完全可以實現(xiàn)(為什么要這么用呢?)
NSLog(@"%@",man.spouse.name);

KVC 尋找 Key 值過程

KVC在某種程度上提供了訪問器的替代方案,不過只要有可能,KVC也是在訪問器方法的幫助下工作。KVC按照以下順序?qū)ふ襅ey值。

1.賦值

當(dāng)程序調(diào)用

- (void)setValue:(nullable id)value forKey:(NSString *)key;

1.優(yōu)先尋找訪問器方法

程序會優(yōu)先調(diào)用 setKey 的屬性值方法,代碼直接通過 Setter 方法完成設(shè)置。這里的 key 值指的是成員變量名,Key 值首字母大寫要符合 SetterGetter 方法的命名規(guī)則。

2.尋找_key

如果沒有找到 setKey 的訪問器方法,KVC 機(jī)制會檢查

+ (BOOL)accessInstanceVariablesDirectly

的返回值是否為NO,此方法默認(rèn)返回的是YES。如果開發(fā)者重寫了該方法讓這個返回值為NO時,接下來KVC會直接調(diào)用

- (void)setValue:(id)value forUndefinedKey:(NSString *)key

這個時候如果你不做其他操作,就要報出異常了,所以一般人都不會這么做。

接下來 KVC 機(jī)制會搜索該類里面有沒有 _key 的成員變量,無論你是在聲明文件中定義,還是在實現(xiàn)文件中定義,也無論使用了什么樣的屬性修飾符,只要存在著 _key 命名的變量,KVC 都可以對該成員變量賦值。

3.尋找_isKey

如果該類既沒有 setKey: 的訪問器方法,也沒有 _key 成員變量,KVC 機(jī)制會搜索 _isKey 的成員變量。

4.尋找Key和isKey

和上面一樣,如果該類既沒有 setKey: 的訪問器方法,也沒有 _key_isKey 成員變量,KVC 機(jī)制再會繼續(xù)搜索 keyisKey 的成員變量,再給它們賦值。

如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的 setValue:forUNdefinedKey: 方法,默認(rèn)是拋出異常。

如果開發(fā)者想讓這個類禁用 KVC ,那么重寫 + (BOOL)accessInstanceVariablesDirectly 方法讓其返回NO即可,這樣的話如果 KVC 沒有找到 set<Key>: 屬性名時,會直接用 setValue:forUNdefinedKey: 方法。

2.取值

當(dāng)程序調(diào)用

- (nullable id)valueForKey:(NSString *)key;

1.優(yōu)先查找訪問器的方法

首先按 getKey 、key、isKey 的順序查找 getter 方法,找到直接調(diào)用。如果是 boolint 等內(nèi)建值類型,會做NSNumber的轉(zhuǎn)換。

2.有序集合中查找

上面的 getter 沒有找到,查找 countOfKey 、 objectInKeyAtIndex:KeyAtIndexes 格式的方法。
如果 countOfKey 和另外兩個方法中的一個找到,那么就會返回一個可以響應(yīng) NSArray 所有方法的代理集合。發(fā)送給這個代理集合的 NSArray 消息方法,就會以countOfKeyobjectInKeyAtIndex:、KeyAtIndexes這幾個方法組合的形式調(diào)用。還有一個可選的 getKey:range: 方法。

3.無序集合中查找

還沒查到,那么查找 countOfKey 、 enumeratorOfKey 、 memberOfKey: 格式的方法。
如果這三個方法都找到,那么就返回一個可以響應(yīng)NSSet所有方法的代理集合。發(fā)送給這個代理集合的NSSet消息方法,就會以countOfKey、enumeratorOfKey、memberOfKey:組合的形式調(diào)用。

4.搜索成員變量名

還是沒查到,那么如果類方法 accessInstanceVariablesDirectly 返回 YES ,那么按 _key_isKey ,key,iskey 的順序直接搜索成員名。

5.報出異常

再找不到,調(diào)用ValueForUndefinedKey:,默認(rèn)報出異常

針對集合類型的 KVC

我們上面講的KVC是一對一關(guān)系,比如 Person 類中的 name 屬性。但也有一對多的關(guān)系,比如 Person 中有一個friends屬性,保存的是一個人的所有的朋友,這時候就需要集合來處理了。

對于集合類的處理,我們有兩種選擇

1.通過KVC將集合類先取出,然后在針對集合進(jìn)行處理

2.采用KVC提供的模板方法

有序集合

這里面的Key,就是被監(jiān)聽的屬性名稱

-countOfKey  
//必須實現(xiàn),對應(yīng)于NSArray的基本方法count:  

- objectInKeyAtIndex:
- keyAtIndexes:  
//這兩個必須實現(xiàn)一個,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes: 
 
- getKey:range:  
//不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應(yīng)于 NSArray 方法 
- getObjects:range:  
  
- insertObject:inKeyAtIndex:  
- insertKey:atIndexes:  
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  

- removeObjectFromKeyAtIndex:  
- removeKeyAtIndexes:  
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:  

- replaceObjectInKeyAtIndex:withObject:  
- replaceKeyAtIndexes:withKey:  
//可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之 

無序集合

- countOfKey 
//必須實現(xiàn),對應(yīng)于NSArray的基本方法count: 
 
- objectInKeyAtIndex:  
- keyAtIndexes:  
//這兩個必須實現(xiàn)一個,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

- getKey:range:  
//不是必須實現(xiàn)的,但實現(xiàn)后可以提高性能,其對應(yīng)于 NSArray 方法 
- getObjects:range:  
  
- insertObject:inKeyAtIndex:  
- insertKey:atIndexes:
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:  

- removeObjectFromKeyAtIndex:  
- removeKeyAtIndexes:  
//兩個必須實現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

- replaceObjectInKeyAtIndex:withObject:  
- replaceKeyAtIndexes:withKey:  
//這兩個都是可選的,如果在此類操作上有性能問題,就需要考慮實現(xiàn)之

KVC對基本數(shù)據(jù)類型和結(jié)構(gòu)體的支持

1.對基本數(shù)據(jù)類型會以 NSNumber 進(jìn)行包裝

+ (NSNumber *)numberWithChar:(char)value;  
+ (NSNumber *)numberWithUnsignedChar:(unsigned char)value;  
+ (NSNumber *)numberWithShort:(short)value;  
+ (NSNumber *)numberWithUnsignedShort:(unsigned short)value;  
+ (NSNumber *)numberWithInt:(int)value;  
+ (NSNumber *)numberWithUnsignedInt:(unsigned int)value;  
+ (NSNumber *)numberWithLong:(long)value;  
+ (NSNumber *)numberWithUnsignedLong:(unsigned long)value;  
+ (NSNumber *)numberWithLongLong:(long long)value;  
+ (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;  
+ (NSNumber *)numberWithFloat:(float)value;  
+ (NSNumber *)numberWithDouble:(double)value;  
+ (NSNumber *)numberWithBool:(BOOL)value;  
+ (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);  
+ (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);

2.對結(jié)構(gòu)體會以 NSValue 進(jìn)行包裝

+ (NSValue *)valueWithCGPoint:(CGPoint)point;  
+ (NSValue *)valueWithCGSize:(CGSize)size;  
+ (NSValue *)valueWithCGRect:(CGRect)rect;  
+ (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;  
+ (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;  
+ (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);  

所有的結(jié)構(gòu)體都支持以NSValue進(jìn)行封裝

KVC中的集合運(yùn)算符

[圖片上傳失敗...(image-739934-1602312865707)]

集合運(yùn)算符是一個特殊的KeyPath,可以作為參數(shù)傳遞給valueForKeyPath:方法

1.簡單的集合運(yùn)算符

簡單的集合運(yùn)算符有以下幾個 @avg,@count,@max@min,@sum5

2.對象運(yùn)算符

對象運(yùn)算符有@distinctUnionOfObjects,
@unionOfObjects,這兩個運(yùn)算符返回的對象都是NSArray。

1.@distinctUnionOfObjects會將集合在剔除重復(fù)對象之后返回

2.@unionOfObjects會直接返回所有對象

NSKeyValueCoding其他方法

+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES,表示如果沒有找到SetKey方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就會直接拋出異常。

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值確認(rèn)的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回

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

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

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

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。

2.KVO

1.認(rèn)識KVO

KVO 類似于觀察者模式,我們利用簡單的代碼來了解什么是 KVO。

//  注冊一個Person類
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@end

// 再注冊一個Dog類
#import <Foundation/Foundation.h>

@interface Dog : NSObject
@property (nonatomic,copy  ) NSString *name;
@end

我們在 ViewController 中引入頭文件,并創(chuàng)建兩個全局的屬性。我們希望Person作為Dog的觀察者,當(dāng)Dogname屬性發(fā)生變化的時候,Person可以第一時間知道。這時我們就可以運(yùn)用KVO的技術(shù)。

Person *p = [Person new];
self.p = p;
Dog *dog = [Dog new];
self.dog = dog;

// 成為其他對象的觀察者要進(jìn)行注冊
// KeyPath代表監(jiān)聽對象的具體屬性
// Observe就是觀察者
// Options可以指定觀察的值的新舊等
// Context可以是任何對象,可以向觀察者傳遞信息,也可以用指定的標(biāo)識對不同的觀察者進(jìn)行區(qū)分

[dog addObserver:p
      forKeyPath:@"name"
         options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
         context:nil];

dog.name = @"旺財";

監(jiān)聽選項Options是由枚舉NSKeyValueObservingOptions定義的,他決定了哪些值可以被傳入到觀察者內(nèi)部實現(xiàn)的方法中。

定義如下:

enum {
       // 提供新值
    NSKeyValueObservingOptionNew = 0x01,
    
    // 提供舊值
    NSKeyValueObservingOptionOld = 0x02,
    
    // 添加觀察者時立即發(fā)送一個通知給觀察者,
    // 并且是在注冊觀察者方法返回之前
    NSKeyValueObservingOptionInitial = 0x04,
    
    // 如果指定,則在每次修改屬性時,會在修改通知被發(fā)送之前預(yù)先發(fā)送一條通知給觀察者,
    // 這與-willChangeValueForKey:被觸發(fā)的時間是相對應(yīng)的。
    // 這樣,在每次修改屬性時,實際上是會發(fā)送兩條通知。
    NSKeyValueObservingOptionPrior = 0x08 
};

typedef NSUInteger NSKeyValueObservingOptions;
//  選項值可以支持多個選項

注冊之后,我們要在觀察者內(nèi)部實現(xiàn)如下方法

// 此時,當(dāng)被觀察者的屬性發(fā)生變更,觀察者就會自動調(diào)用如下方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    // keyPath被觀察的屬性值
    NSLog(@"keyPath = %@",keyPath);
    
    // object被觀察的對象
    NSLog(@"object = %@",object);
    
    // 被觀察屬性值得變化,后面還會講
    NSLog(@"change = %@",change);
    
    // 上下文,也可以是任意的額外數(shù)據(jù)
    // 這個Context的作用十分重要,我在后面會強(qiáng)調(diào)
    NSLog(@"context = %@",context);
}
// 我們通過這個方法,可以得到一些關(guān)鍵信息

Change選項,它記錄了被監(jiān)聽屬性的變化情況??梢酝ㄟ^key來獲取值:


// 屬性變化的類型,是一個NSNumber對象,包含NSKeyValueChange枚舉相關(guān)的值
NSString *const NSKeyValueChangeKindKey;

// 屬性的新值。當(dāng)NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加觀察的方法設(shè)置了NSKeyValueObservingOptionNew時,我們能獲取到屬性的新值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionNew時,則我們能獲取到一個NSArray對象,包含被插入的對象或
// 用于替換其它對象的對象。
NSString *const NSKeyValueChangeNewKey;

// 屬性的舊值。當(dāng)NSKeyValueChangeKindKey是 NSKeyValueChangeSetting,
// 且添加觀察的方法設(shè)置了NSKeyValueObservingOptionOld時,我們能獲取到屬性的舊值。
// 如果NSKeyValueChangeKindKey是NSKeyValueChangeRemoval或者NSKeyValueChangeReplacement,
// 且指定了NSKeyValueObservingOptionOld時,則我們能獲取到一個NSArray對象,包含被移除的對象或
// 被替換的對象。
NSString *const NSKeyValueChangeOldKey;

// 如果NSKeyValueChangeKindKey的值是NSKeyValueChangeInsertion、NSKeyValueChangeRemoval
// 或者NSKeyValueChangeReplacement,則這個key對應(yīng)的值是一個NSIndexSet對象,
// 包含了被插入、移除或替換的對象的索引
NSString *const NSKeyValueChangeIndexesKey;

// 當(dāng)指定了NSKeyValueObservingOptionPrior選項時,在屬性被修改的通知發(fā)送前,
// 會先發(fā)送一條通知給觀察者。我們可以使用NSKeyValueChangeNotificationIsPriorKey
// 來獲取到通知是否是預(yù)先發(fā)送的,如果是,獲取到的值總是@(YES)
NSString *const NSKeyValueChangeNotificationIsPriorKey;

NSKeyValueChangeKindKey的值取自于NSKeyValueChange,這是一個枚舉值,定義如下

enum {
    // 設(shè)置一個新值。被監(jiān)聽的屬性可以是一個對象,也可以是一對一關(guān)系的屬性或一對多關(guān)系的屬性。
    NSKeyValueChangeSetting = 1,
    
    // 表示一個對象被插入到一對多關(guān)系的屬性。
    NSKeyValueChangeInsertion = 2,
    
    // 表示一個對象被從一對多關(guān)系的屬性中移除。
    NSKeyValueChangeRemoval = 3,
    
    // 表示一個對象在一對多的關(guān)系的屬性中被替換
    NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;

注意,觀察者在不需要使用的時候一定要移除,否則會產(chǎn)生崩潰

- (void)dealloc {

    [self.dog removeObserver:self.p forKeyPath:@"name"];
}

通過上面簡要的代碼示例,我們可以得知,時運(yùn)觀察者只需要實現(xiàn)簡單的幾步。

  1. 注冊觀察者
  2. 觀察者實現(xiàn)相應(yīng)的方法
  3. 移除觀察者

2.KVC和KVO的實現(xiàn)原理

KVCKVO是基于強(qiáng)大的Runtime來實現(xiàn)的。其中使用到的技術(shù)就是isa-swilling,isa-swilling這項技術(shù)也是一個重點,我們會在后續(xù)的 Runtime 部分會講到。如果有看到此處不明白的同學(xué)也請保持耐心。

網(wǎng)上有一篇文章針對實現(xiàn)原理寫的很好,鏈接在此。

整體來說就是,當(dāng)某個類的對象第一次被觀察時,系統(tǒng)會在運(yùn)行期間動態(tài)的為這個類創(chuàng)建一個派生類,假如被監(jiān)聽類名為ClassA,那么派生類的名稱就為NSKVONotifying_ClassA

1.原有對象的isa指針會指向全新的派生類,派生類為了混淆,避免別人知道他不是原來的類,所以派生類重寫了Class的類方法。

2.同時重寫了Dealloc方法,用于資源的銷毀處理。

3.還重寫了_isKVOA,這個是一個標(biāo)記,用于標(biāo)示這個類是遵守KVO機(jī)制的。

4.最關(guān)鍵的是重寫了被監(jiān)聽屬性的Setter方法,這是實現(xiàn)KVO的關(guān)鍵。至于為什么,后面會講解到。

簡單的畫了張圖,可能會有助于理解。
[圖片上傳失敗...(image-74ea05-1602312865707)]

我們上面講重寫了被觀察對象屬性的Setter方法是十分關(guān)鍵的,這就要說起另外兩個十分重要的方法

// 在屬性值即將被修改的時候,會調(diào)用這個方法
- (void)willChangeValueForKey:(NSString *)key;

// 在屬性值已經(jīng)被修改的時候,會調(diào)用這個方法
- (void)didChangeValueForKey:(NSString *)key;

// didChangeValueForKey:方法會顯式的調(diào)用
- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                       context:(void *)context {
                       }

其實我個人猜測,重寫Setter方法內(nèi)部應(yīng)該這樣實現(xiàn)的

[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];

說到這里,相信你應(yīng)該完整的明白KVO的實現(xiàn)機(jī)制了。

// 這才是KVO機(jī)制觸發(fā)的關(guān)鍵
- (void)didChangeValueForKey:(NSString *)key;

3.調(diào)用KVO的三種方法

綜合上面KVO的實現(xiàn)原理,我們可以得出如下結(jié)論:

1.使用了KVC

使用了 KVC ,如果有 訪問器方法 ,則運(yùn)行時會在訪問器方法中調(diào)用 will/didChangeValueForKey: 方法;
沒用訪問器方法,運(yùn)行時會在setValue:forKey方法中調(diào)用will/didChangeValueForKey:方法。

2.有訪問器方法

運(yùn)行時會重寫訪問器方法調(diào)用will/didChangeValueForKey:方法。
因此,直接調(diào)用訪問器方法改變屬性值時,KVO也能監(jiān)聽到。

3.直接調(diào)用

顯式調(diào)用will/didChangeValueForKey:方法。

4.KVO自動通知、手動通知

通常意義下我們使用的都是自動通知,注冊觀察者之后,當(dāng)觸發(fā)will/didChangeValueForKey:方法后,觀察者對象的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { }方法會被調(diào)用。

如果想實現(xiàn)手動通知,我們需要借助一個額外的方法

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

這個方法默認(rèn)返回YES,用來標(biāo)記Key指定的屬性是否支持KVO,如果返回值為NO,則需要我們手動更新。

我們還是用我們最上面的例子,監(jiān)聽Personname屬性,不過這次我們采取手動通知的方式。

#import "Person.h"

@implementation Person

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {

    BOOL automaic = NO;
    if ([key isEqualToString:@"name"])
    {
        automaic = NO;
    }
    else
    {
        // 此處需要注意,沒有被處理的其他屬性要調(diào)用父類的原有方法
        automaic = [super automaticallyNotifiesObserversForKey:key];
    }
    return automaic;
}
@end


這樣我們就已經(jīng)標(biāo)記好當(dāng)Personname屬性發(fā)生改變時,手動發(fā)送通知,代碼如下:

@implementation Person

- (void)setName:(NSString *)name {
    
    if(name != _name)// 加一處判斷,如果值相同,就無需發(fā)送通知了
    {   
        // 我們需要在值修改前調(diào)用`will...`方法
        [self willChangeValueForKey:@"name"];
        _name = name;
        // 我們還需要在修改后調(diào)用`did...`方法,顯式調(diào)用觀察者的方法
        [self didChangeValueForKey:@"name"];
    }
}
@end

手動發(fā)送通知一對一的操作方法如上,如果是一對多的案例,則可以使用如下方法

- (void)willChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
- (void)didChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
  
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects

5.注冊依賴鍵(類似于 Vue 里面的計算屬性)

實際開發(fā)過程中可能會遇到這種場景,某個變量的值取決于其它的值。

我們還是看一個例子吧:

// 聲明一個Person類,有三個屬性
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic,copy) NSString *fullName;
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,copy) NSString *lastName;

@end

// 其中 fullName 取決于 firstName 和 lastName的值.
// 同時如果 firstName 和 lastName發(fā)生改變的話,fullName也會受到影響。

#import "Person.h"

@implementation Person

// 注冊 fullName依賴于 firstName 和 lastName
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {

    return [NSSet setWithObjects:@"firstName",@"lastName",nil];
}

- (NSString *)fullName {

    NSString *tempName = _fullName;
    
    if (_firstName || _lastName)
    {
        tempName = [NSString stringWithFormat:@"%@-%@",_firstName,_lastName];
    }

    return tempName;
}

回到Controller:

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    Person *p = [Person new];
    self.p = p;
    
    [self.p  addObserver:self
        forKeyPath:NSStringFromSelector(@selector(name))
           options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
           context:ContextMark];
    
    self.p.fullName = @"lilei";
    NSLog(@"fullName = %@",self.p.fullName);
    
    self.p.firstName = @"lala";
    NSLog(@"fullName = %@",self.p.fullName);

    self.p.lastName = @"papa";
    NSLog(@"fullName = %@",self.p.fullName);
}

// 打印結(jié)果如下
fullName = lilei
fullName = lala-(null)
fullName = lala-papa

6.KVO使用中的"坑"

最近我在看這方面資料的時候,發(fā)現(xiàn)大家都以 tableViewContentOffset作為例子。咱們就用這個最常見的控件來說明一下吧。

1.keyPath為字符串

眾所周知,KVO里面的KeyPathNSString類型,結(jié)合Obj-C動態(tài)語言的特性,在編譯時是不做檢查的,只有運(yùn)行到執(zhí)行的時候,才會動態(tài)的去方法列表、實例變量列表中去查找,所以一旦我們寫錯了KeyPath,不運(yùn)行的時候很難發(fā)現(xiàn)。

基于這個問題,我們用以下的方法規(guī)避

// 這樣就不會寫錯了
NSStringFromSelector(@selector(contentSize))

2.多層繼承、共用同一個回調(diào)方法

假如父類的控制器監(jiān)聽了tableViewContentOffset屬性,同時該控制器還監(jiān)聽了其他控件的一些屬性,但是同一個對象或者控制器作為多個對象屬性的觀察者,實際上最后調(diào)用的都是同一個回調(diào)方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { },這樣寫極其容易混淆,所以我們?yōu)榱私鉀Q這個問題,把代碼寫成如下的樣子

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) 
    {
        [self doSomethingWhenContentOffsetChanges];
   } 
}

但是光這樣寫是不全面的,因為當(dāng)前的這個類很可能有父類,并且它的父類可能綁定了一些其他的KVO,上面的代碼只有一個條件判斷,一旦不成立,此次KVO的觸發(fā)操作也就斷了。而當(dāng)前類無法捕捉的這個KVO事件很可能就在它的父類里,或者是父類的父類,上述操作,將這一鏈條截斷,所以正確的方法應(yīng)該如下:

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    if (object == _tableView && [keyPath isEqualToString:@"contentOffset"]) 
    {
        [self doSomethingWhenContentOffsetChanges];
   } 
   else
   {
       [super observeValueForKeyPath:keyPath 
                            ofObject:object 
                              change:change 
                             context:context];
   }
}

這樣做這一鏈條就完整的保留了。

3.觀察者的注銷

上面的方法做完之后還是有隱患的。我們知道KVO不用的時候是需要注銷的。我們知道當(dāng)你對同一個KVO注銷兩次的時候,系統(tǒng)默認(rèn)是拋出異常的。

你可能會好奇,什么時候我會對同一個Observer注銷多次呢?

這個時候我們可以想一下我們注銷Observer的時機(jī),是不是多在Dealloc方法中?

Obj-C中,有很多系統(tǒng)的方法被重寫時需要調(diào)用super xxxxxxx等方法,這是Obj-C的繼承關(guān)系決定的。

例如:

// 在重寫init方法時,我們要調(diào)用一下父類的init方法
- (instancetype)init {

    [super init];
}

// 布局子控件時,要調(diào)用一下父類的layoutSubviews方法
- (void)layoutSubviews {

    [super layoutSubviews];
}

還有些方法,不需要調(diào)用父類的方法,自動就會幫你調(diào)用,就如我們所說的Dealloc。其實只有在ARC模式下才不需要調(diào)用父類,MRC下的Dealloc還是要手動調(diào)用super dealloc的。

所以我們在注銷觀察者的時候就這么寫

- (void)dealloc {

    [_tableView removeObserver:self forKeyPath:@"contentOffset"];
}

假設(shè)我們有三個類 ClassA(父類),ClassB(子類),ClassC(孫子類)。這三個類都作為觀察者,觀察tableViewcontentOffset屬性。

如果我們在ClassC(孫子類)Dealloc方法中釋放觀察者

- (void)dealloc {

    [_tableView removeObserver:self forKeyPath:@"contentOffset"];
}

當(dāng)ClassC(孫子類)Dealloc執(zhí)行完畢后,就會自動去ClassB(子類)Dealloc方法中,釋放觀察者

- (void)dealloc {

    [_tableView removeObserver:self forKeyPath:@"contentOffset"];
}

這個時候就出現(xiàn)崩潰了,因為我們在前面提到過這樣會導(dǎo)致相同的removeObserver被執(zhí)行兩次,于是導(dǎo)致crash。

4.正確寫法

針對這種類型的Crash,我們就要談一下在注冊Observer似的一個關(guān)鍵的參數(shù)Context,之前我是不知道這個Context是做啥用的,對于KVO的使用只是流于表面,所以對于這個神秘的Context的作用一直沒有深究,現(xiàn)在我們將使用Context來為每一個Observer做區(qū)分,避免多次調(diào)用相同的removeObserver。

KVO的三個關(guān)鍵方法

//  注冊觀察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

// 觀察者響應(yīng)方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

// 移除觀察者(有兩個方法)
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

相比細(xì)心的同學(xué)已經(jīng)看出來了,我們在注冊、響應(yīng)、移除的三個步驟里,都可以找到Context這個關(guān)鍵字。所以為了保持注冊、響應(yīng)、移除的一致性,正確的寫法應(yīng)該如下:

// 首先我們應(yīng)在使用KVO的類中,創(chuàng)建一個獨一無二的Context,用來和其他類進(jìn)行區(qū)分
static Void *ContextMark = &ContextMark;

// 接下來注冊的時候用
 [_tableView addObserver:self
              forKeyPath:NSStringFromSelector(@selector(contentSize))
                 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                 context:ContextMark];
                 
// 響應(yīng)的時候用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    
    if (context == ContextMark)
    {
        // do someThing
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

// 注銷的時候用
- (void)dealloc {
    [_tableView removeObserver:self 
                    forKeyPath:NSStringFromSelector(@selector(contentSize)) 
                       context:ContextMark];
}

如果還不放心,也可以使用@try @catch去捕獲異常

7.總結(jié)、

[圖片上傳失敗...(image-7a5c0a-1602312865707)]

KVO 這套 API 真麻煩~~~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容