本文是Objective-C系列的第9篇,主要講述了KVO的底層實現,以及KVC的使用及KVC中調用流程。html
KVO
全稱Key Value Observing
,是蘋果提供的一套事件通知機制。容許對象監聽另外一個對象特定屬性的改變,並在改變時接收到事件。因爲KVO
的實現機制,只針對屬性纔會發生做用,通常繼承自NSObject
的對象都默認支持KVO
。git
KVO
和NSNotificationCenter
都是iOS
中觀察者模式的一種實現。區別在於,相對於被觀察者和觀察者之間的關係,KVO
是一對一的,而不是一對多的。KVO
對被監聽對象無侵入性,不須要修改其內部代碼便可實現監聽。github
KVO
能夠監聽單個屬性的變化,也能夠監聽集合對象的變化。經過KVC
的mutableArrayValueForKey:
等方法得到代理對象,當代理對象的內部對象發生改變時,會回調KVO
監聽的方法。集合對象包含NSArray
和NSSet
。api
項目代碼KVO-01-usage數組
- (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"];
}
複製代碼
/** 觀察者監聽的回調方法 @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);
}
複製代碼
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 29;
self.person1.name = @"hengcong";
}
複製代碼
//下面調用方式均可以出發KVO
self.person1.age = 29;
[self.person1 setAge:29];
[self.person1 setValue:@(29) forKey:@"age"];
[self.person1 setValue:@(29) forKeyPath:@"age"];
複製代碼
KVO在屬性發生改變時的調用是自動的,若是想要手動控制這個調用時機,或想本身實現KVO屬性的調用,則能夠經過KVO提供的方法進行調用。bash
下面以age
屬性爲例:app
//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。工具
- (void)setAge:(NSInteger)age
{
if (_age != age) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
}
複製代碼
實現了(1)禁用自動調用(2)手動調用實現 兩步,age
屬性手動調用就實現了,此時能和自動調用同樣,觸發KVO。
- (void)dealloc
{
[self removeObserver];
}
- (void)removeObserver
{
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"name"];
}
複製代碼
KVO若使用不當,極容易引起Crash。相關試驗代碼在KVO-02-crash
若觀察者對象**-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.
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";
}
複製代碼
假如在當前ViewController中,註冊了觀察者,點擊屏幕,改變被觀察對象person1的屬性值。
點擊對應按鈕,銷燬觀察者,此時self.observerPersonChange爲nil。
再次點擊屏幕,此時Crash;
Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path "age" from <BFPerson 0x6000014acd00> because it is not registered as an observer.
在註冊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"];
複製代碼
/** 若是age改變 觀察者也會收到name改變的通知 */
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAge
{
NSSet *set = [NSSet setWithObjects:@"name", nil];
return set;
}
複製代碼
爲了區分在添加KVO以後,對象以及對應的屬性設值方法發生的變化,咱們進行了以下測試:
觀察方法實現:
person1
指向的類對象
和元類對象
,以及setAge:
均發生了變化;person1
中的isa
指向了NSKVONotifying_BFPerson類對象;setAge:
的實現調用的是:Foundation 中 _NSSetLongLongValueAndNotify
方法;重寫項目在KVO-03-princlipe。
咱們經過重寫方法後,進行打印測試
結合
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
咱們整理出一個完整的方法鏈:
以下圖:
可是這些不足以反應真正完整的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 theclass
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_BFPerson
的superclass
,找到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
類已經成型(相關代碼參考項目),結合調用流程,咱們繪製出下面對比圖。
項目源碼在:KVC-01-usage
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
複製代碼
其中,有兩個方法要注意:
valueForKey | objectForKey | |
---|---|---|
無key的處理 | 無該key,crash,NSUndefinedKeyException | 無該key返回nil |
來源 | KVC主要方法 | NSDictionary的方法 |
符號 | 若以 @ 開頭,去掉 @ ,用剩下部分做爲 key 執行 [super valueForKey:] | key 不是以 @ 符號開頭, 二者等同 |
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;
複製代碼
有序集合對應方法以下:
-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>://這兩個都是可選的,若是在此類操做上有性能問題,就須要考慮實現之
複製代碼
例如設置: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);
}
複製代碼
當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每個對象,而不是容器自己進行操做。結果會被添加進返回的容器中,這樣,開發者能夠很方便的操做集合來返回另外一個集合。
NSArray *arr = @[@"ali",@"bob",@"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str in arrCap) {
NSLog(@"%@",str); //Ali\Bob\Cydia
}
複製代碼
簡單集合運算符
@interface Book : NSObject
@property (nonatomic,assign) CGFloat price;
@end
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
複製代碼
對象運算符
// 獲取全部Book的price組成的數組,而且去重
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
複製代碼
Array和Set操做符(集合中包含集合的情形)
項目源碼在**KVC-02-principle**
setValue:forKey
valueForKey
//抽出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
複製代碼
KVOController Facebook出品的KVO封裝庫。