KVC、KVO 相關知識點

1.KVC

關于 KVCKVO ,我之前的總結文章有寫過,但是趨于表面,沒有探究其內部真正的實現原理和進階用法,這次總結正好給了我很好的學習機會,在此深入的總結一下 KVCKVO

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

Api示例

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

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

一般來講,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 對應他們的 Key 值。

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

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

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

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

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

可以這樣操作

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

簡單對比一下

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

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

KVC 尋找 Key 值過程

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

1.賦值

當程序調用

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

1.優先尋找訪問器方法

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

2.尋找_key

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

+ (BOOL)accessInstanceVariablesDirectly

的返回值是否為NO,此方法默認返回的是YES。如果開發者重寫了該方法讓這個返回值為NO時,接下來KVC會直接調用

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

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

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

3.尋找_isKey

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

4.尋找Key和isKey

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

如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的 setValue:forUNdefinedKey: 方法,默認是拋出異常。

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

2.取值

當程序調用

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

1.優先查找訪問器的方法

首先按 getKeykeyisKey 的順序查找 getter 方法,找到直接調用。如果是 boolint 等內建值類型,會做NSNumber的轉換。

2.有序集合中查找

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

3.無序集合中查找

還沒查到,那么查找 countOfKeyenumeratorOfKeymemberOfKey: 格式的方法。
如果這三個方法都找到,那么就返回一個可以響應NSSet所有方法的代理集合。發送給這個代理集合的NSSet消息方法,就會以countOfKeyenumeratorOfKeymemberOfKey:組合的形式調用。

4.搜索成員變量名

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

5.報出異常

再找不到,調用ValueForUndefinedKey:,默認報出異常

針對集合類型的 KVC

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

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

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

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

有序集合

這里面的Key,就是被監聽的屬性名稱

-countOfKey  
//必須實現,對應于NSArray的基本方法count:  

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

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

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

無序集合

- countOfKey 
//必須實現,對應于NSArray的基本方法count: 
 
- objectInKeyAtIndex:  
- keyAtIndexes:  
//這兩個必須實現一個,對應于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

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

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

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

KVC對基本數據類型和結構體的支持

1.對基本數據類型會以 NSNumber 進行包裝

+ (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.對結構體會以 NSValue 進行包裝

+ (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);  

所有的結構體都支持以NSValue進行封裝

KVC中的集合運算符

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

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

1.簡單的集合運算符

簡單的集合運算符有以下幾個 @avg@count@max@min@sum5

2.對象運算符

對象運算符有@distinctUnionOfObjects,
@unionOfObjects,這兩個運算符返回的對象都是NSArray

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

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

NSKeyValueCoding其他方法

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

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

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

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

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

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

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

2.KVO

1.認識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 中引入頭文件,并創建兩個全局的屬性。我們希望Person作為Dog的觀察者,當Dogname屬性發生變化的時候,Person可以第一時間知道。這時我們就可以運用KVO的技術。

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

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

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

dog.name = @"旺財";

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

定義如下:

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

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

注冊之后,我們要在觀察者內部實現如下方法

// 此時,當被觀察者的屬性發生變更,觀察者就會自動調用如下方法
- (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);
    
    // 上下文,也可以是任意的額外數據
    // 這個Context的作用十分重要,我在后面會強調
    NSLog(@"context = %@",context);
}
// 我們通過這個方法,可以得到一些關鍵信息

Change選項,它記錄了被監聽屬性的變化情況。可以通過key來獲取值:


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

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

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

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

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

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

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

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

- (void)dealloc {

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

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

  1. 注冊觀察者
  2. 觀察者實現相應的方法
  3. 移除觀察者

2.KVC和KVO的實現原理

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

網上有一篇文章針對實現原理寫的很好,鏈接在此

整體來說就是,當某個類的對象第一次被觀察時,系統會在運行期間動態的為這個類創建一個派生類,假如被監聽類名為ClassA,那么派生類的名稱就為NSKVONotifying_ClassA

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

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

3.還重寫了_isKVOA,這個是一個標記,用于標示這個類是遵守KVO機制的。

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

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

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

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

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

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

其實我個人猜測,重寫Setter方法內部應該這樣實現的

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

說到這里,相信你應該完整的明白KVO的實現機制了。

// 這才是KVO機制觸發的關鍵
- (void)didChangeValueForKey:(NSString *)key;

3.調用KVO的三種方法

綜合上面KVO的實現原理,我們可以得出如下結論:

1.使用了KVC

使用了 KVC ,如果有 訪問器方法 ,則運行時會在訪問器方法中調用 will/didChangeValueForKey: 方法;
沒用訪問器方法,運行時會在setValue:forKey方法中調用will/didChangeValueForKey:方法。

2.有訪問器方法

運行時會重寫訪問器方法調用will/didChangeValueForKey:方法。
因此,直接調用訪問器方法改變屬性值時,KVO也能監聽到。

3.直接調用

顯式調用will/didChangeValueForKey:方法。

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

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

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

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

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

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

#import "Person.h"

@implementation Person

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

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


這樣我們就已經標記好當Personname屬性發生改變時,手動發送通知,代碼如下:

@implementation Person

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

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

- (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 里面的計算屬性)

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

我們還是看一個例子吧:

// 聲明一個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發生改變的話,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);
}

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

6.KVO使用中的"坑"

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

1.keyPath為字符串

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

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

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

2.多層繼承、共用同一個回調方法

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

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

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

- (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不用的時候是需要注銷的。我們知道當你對同一個KVO注銷兩次的時候,系統默認是拋出異常的。

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

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

Obj-C中,有很多系統的方法被重寫時需要調用super xxxxxxx等方法,這是Obj-C的繼承關系決定的。

例如:

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

    [super init];
}

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

    [super layoutSubviews];
}

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

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

- (void)dealloc {

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

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

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

- (void)dealloc {

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

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

- (void)dealloc {

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

這個時候就出現崩潰了,因為我們在前面提到過這樣會導致相同的removeObserver被執行兩次,于是導致crash。

4.正確寫法

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

KVO的三個關鍵方法

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

// 觀察者響應方法
- (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;

相比細心的同學已經看出來了,我們在注冊響應移除的三個步驟里,都可以找到Context這個關鍵字。所以為了保持注冊響應移除的一致性,正確的寫法應該如下:

// 首先我們應在使用KVO的類中,創建一個獨一無二的Context,用來和其他類進行區分
static Void *ContextMark = &ContextMark;

// 接下來注冊的時候用
 [_tableView addObserver:self
              forKeyPath:NSStringFromSelector(@selector(contentSize))
                 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                 context:ContextMark];
                 
// 響應的時候用
- (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.總結、

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容