Runtime系列導讀
KVO簡介
全稱Key-Value Observing,KVO是Object-C中定義的一個通知機制,其定義了一種對象間監控對方狀態的改變,并做出反應的機制。對象可以為自己的屬性注冊觀察者,當這個屬性的值發生了改變,系統會對這些注冊的觀察者做出通知。
KVO用法
添加監聽
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
- observer: 觀察者對象. 其必須實現方法observeValueForKeyPath:ofObject:change:context:
- keyPath: 被觀察的屬性,不能為nil
- options: 設定通知觀察者時傳遞的屬性值的類型,具體設置可查看枚舉
NSKeyValueObservingOptions
- context: 一些其他的需要傳遞給觀察者的上下文信息,通常設置為nil
監聽實現
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
- keyPath: 被觀察的屬性
- object: 被觀察的對象
- change: 根據options設置,可能出現old|new,或者都有
- context: 監聽時傳入的上下文信息
KVO實現過程
KVO的實現過程實際上是利用了OC的runtime機制,當一個實例對象添加觀察者時,底層根據該實例對象所屬的類動態添加了一個類(動態添加的類名就是在原來類的類名前加上NSKVONotifying_前綴),這個類是繼承自原來的類的。這里以繼承自NSObject的KVOTest
類來舉例。
- KVOTest實現:
@interface KVOTest : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOTest
-(void)didChangeValueForKey:(NSString *)key
{
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey:%@,%p", key, self);
}
@end
- 調用代碼:
-(void)testKVO2
{
self.test = [KVOTest new];
self.test.age = 10;
NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.test addObserver:self forKeyPath:@"age" options:option context:nil];
self.test.age = 10;
}
- 監聽代碼:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
NSLog(@"%@ - %@" , keyPath, change);
}
- 打印日志:
2022-06-25 21:58:32.716895+0800 StudyApp[31571:1344036] age - {
kind = 1;
new = 10;
old = 10;
}
2022-06-25 21:58:32.716960+0800 StudyApp[31571:1344036] didChangeValueForKey:age,0x600003f14590
上面實例的底層實現過程如下:
- self.test添加觀察者時,底層就利用runtime動態生成一個叫NSKVONotifying_KVOTest的類,這個類繼承自KVOTest類,并重寫了以下實例方法:
- 重寫class方法,不重寫的話調用這個方法返回的是NSKVONotifying_KVOTest這個類,重寫后返回的是原本的KVOTest類。蘋果這么做的目的是為了隱藏KVO的實現細節。
- 重寫dealloc方法,在這個方法里面做一些收尾的工作。
- 重寫_isKVOA方法,這是一個私有方法,我們不必關心。
- 重寫被監聽屬性的setter方法,上面案例只監聽了name屬性,所以只需重寫setName:方法。重寫setter是實現KVO的關鍵,在setter方法里面實際是調用的Foundation框架下的_NSSetValueAndNotify方法(表示不是一個固定的,這個和監聽的屬性的類型有關,比如是屬性是int類型的話這里就是__NSSetIntValueAndNotify,所包含的類型會在后面列出來)。
- 然后將self.test這個實例對象的isa改為指向NSKVONotifying_KVOTest(原本是指向KVOTest類的)。
- 當我們設置被監聽屬性的值時self.test.age = 10,是調用的setAge:方法,前面說了setAge:方法被重寫了,所以實際上調用的是_NSSetIntValueAndNotify這個方法。這個方法實現蘋果是沒有開源的,無法得知其具體實現,不過可以猜出其實現流程大致如下:
- 首先調用[self willChangeValueForKey:@"age"];這個方法。
- 然后調用原先的setter方法的實現(比如_age = age;);
- 再調用[self didChangeValueForKey:@"age"];這個方法。
- 最后在didChangeValueForKey:這個方法中調用觀察者的observeValueForKeyPath: ofObject: change: context:方法來通知觀察者屬性值發生了變化。
KVO答疑
如何手動觸發KVO?
- 手動調用willChangeValueForKey: 和 didChangeValueForKey:
直接修改成員變量會觸發KVO嗎?
- 不會觸發KVO
對同一個屬性N次注冊,修改一次該屬性,observeValueForKeyPath會調用幾次
N次。
對同一個屬性一次注冊,多次removeObserver,會發生什么
crash,提示**'Cannot remove an observer <ViewController 0x7fa3fd708c60> for the key path "age" from <KVOTest 0x6000018e8790> because it is not registered as an observer.'**