1.KVC
關于 KVC
和 KVO
,我之前的總結文章有寫過,但是趨于表面,沒有探究其內部真正的實現原理和進階用法,這次總結正好給了我很好的學習機會,在此深入的總結一下 KVC
和 KVO
。
KVC,即是指 NSKeyValueCoding,一個非正式的 Protocol,提供一種機制來間接訪問對象的屬性。KVO 就是基于 KVC 實現的關鍵技術之一,相關的技術還有 Cocoa 綁定,Core Data 和 AppleScript。
Api示例
Objective-C
中 KVC
的定義是對 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
這里讀出woman
的name
屬性
可以這樣操作
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
值首字母大寫要符合 Setter
和 Getter
方法的命名規則。
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
機制再會繼續搜索 key
和 isKey
的成員變量,再給它們賦值。
如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的 setValue:forUNdefinedKey:
方法,默認是拋出異常。
如果開發者想讓這個類禁用 KVC
,那么重寫 + (BOOL)accessInstanceVariablesDirectly
方法讓其返回NO即可,這樣的話如果 KVC
沒有找到 set<Key>:
屬性名時,會直接用 setValue:forUNdefinedKey:
方法。
2.取值
當程序調用
- (nullable id)valueForKey:(NSString *)key;
1.優先查找訪問器的方法
首先按 getKey
、key
、isKey
的順序查找 getter
方法,找到直接調用。如果是 bool
、 int
等內建值類型,會做NSNumber
的轉換。
2.有序集合中查找
上面的 getter
沒有找到,查找 countOfKey
、 objectInKeyAtIndex:
、 KeyAtIndexes
格式的方法。
如果 countOfKey
和另外兩個方法中的一個找到,那么就會返回一個可以響應 NSArray
所有方法的代理集合。發送給這個代理集合的 NSArray
消息方法,就會以countOfKey
、objectInKeyAtIndex:
、KeyAtIndexes
這幾個方法組合的形式調用。還有一個可選的 getKey:range:
方法。
3.無序集合中查找
還沒查到,那么查找 countOfKey
、 enumeratorOfKey
、 memberOfKey:
格式的方法。
如果這三個方法都找到,那么就返回一個可以響應NSSet所有方法的代理集合。發送給這個代理集合的NSSet
消息方法,就會以countOfKey
、enumeratorOfKey
、memberOfKey:
組合的形式調用。
4.搜索成員變量名
還是沒查到,那么如果類方法 accessInstanceVariablesDirectly
返回 YES
,那么按 _key
,_isKey
,key
,iskey
的順序直接搜索成員名。
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
的觀察者,當Dog
的name
屬性發生變化的時候,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"];
}
通過上面簡要的代碼示例,我們可以得知,時運觀察者只需要實現簡單的幾步。
- 注冊觀察者
- 觀察者實現相應的方法
- 移除觀察者
2.KVC和KVO的實現原理
KVC
和KVO
是基于強大的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
,則需要我們手動更新。
我們還是用我們最上面的例子,監聽Person
的name
屬性,不過這次我們采取手動通知的方式。
#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
這樣我們就已經標記好當Person
的name
屬性發生改變時,手動發送通知,代碼如下:
@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使用中的"坑"
最近我在看這方面資料的時候,發現大家都以
tableView
和ContentOffset
作為例子。咱們就用這個最常見的控件來說明一下吧。
1.keyPath為字符串
眾所周知,KVO里面的KeyPath
是NSString
類型,結合Obj-C
動態語言的特性,在編譯時是不做檢查的,只有運行到執行的時候,才會動態的去方法列表
、實例變量列表
中去查找,所以一旦我們寫錯了KeyPath
,不運行的時候很難發現。
基于這個問題,我們用以下的方法規避
// 這樣就不會寫錯了
NSStringFromSelector(@selector(contentSize))
2.多層繼承、共用同一個回調方法
假如父類的控制器監聽了tableView
的ContentOffset
屬性,同時該控制器還監聽了其他控件的一些屬性,但是同一個對象或者控制器作為多個對象屬性的觀察者,實際上最后調用的都是同一個回調方法- (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(孫子類)
。這三個類都作為觀察者,觀察tableView
的contentOffset
屬性。
如果我們在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 真麻煩~~~~~