想要回答這個問題,首先須要弄明白當給一個對象的屬性添加KVO後,系統作了哪些事情?git
因此,KVO的本質是修改屬性的setter方法,在屬性的setter方法裏添加調用監聽方法的邏輯,爲了避免破壞原始類,系統又增長了動態建立子類並修改對象的isa指針的機制。github
想要知道如何手動觸發KVO,首先須要弄明白系統是如何修改setter方法以調用監聽方法的。數組
_NSSetXXXValueAndNotify
函數_NSSetXXXValueAndNotify
函數內部會調用 willChangeValueForKey:
和 didChangeValueForKey:
方法didChangeValueForKey:
內部會調用KVO監聽者的監聽方法因此,想要手動觸發KVO,能夠經過手動調用willChangeValueForKey:
和didChangeValueForKey:
來實現,須要強調這兩個方法都必須調用纔會起做用bash
手動實現KVO的過程就是把系統實現KVO的過程本身用代碼實現一下。大體流程以下:app
objc_allocateClassPair
函數動態建立子類class_addMethod
函數重寫setter方法詳細內容可參考iOS_KVO_Study 框架
不會觸發,直接修改爲員變量並不會觸發setter方法,所以也就不會觸發KVO函數
KVO全稱 Key-Value Observing,鍵值監聽。 ui
[p1 addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
複製代碼
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
//...實現監聽處理
}
複製代碼
[self removeObserver:self forKeyPath:@「age"]; 複製代碼
在添加監聽後,age屬性的值在發生改變時就會通知監聽者,執行監聽者的observeValueForKeyPath
方法。接下來咱們就一步步探究爲什麼會在age值發生改變後通知監聽者。spa
賦值操做會調用set方法,咱們經過重寫Person類的setAge:方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。指針
// ViewController類
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 1;
p1.age = 2;
p2.age = 2;
// self 監聽 p1的 age屬性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change);
}
// Person類
- (void)setAge:(NSInteger)age {
_age = age;
}
複製代碼
經過觀察發現p1和p2一樣調用了setAge:方法,p1除了調用setAge:方法外還會執行監聽者的observeValueForKeyPath方法。顯然這些並非在setAge:方法中調用的。
既然不是經過修改setAge:方法來實現監聽的,那addObserver方法對p1對象作了什麼特殊處理呢?咱們經過打印isa指針來進行對比。
NSKVONotifying_原類
NSLog(@"添加KVO監聽以前 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethods:object_getClass(p2)];
[self printMethods:object_getClass(p1)];
NSLog(@"添加KVO監聽以後 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
複製代碼
經過打印的地址信息,咱們發如今添加KVO監聽以前,p1和p2的setAge:方法實現的地址相同,而通過KVO監聽以後,p1的setAge方法實現的地址發生了變化,p1的setAge:方法的實現轉換爲了C語言的Foundation框架的 _NSSetLongLongValueAndNotify
函數。
NSKVONotifying_Person
的內部結構- (void)printMethods:(Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
複製代碼
打印內容以下:
setAge:
class
dealloc
_isKVOA
,下圖是NSKVONotifying_Person的內存結構以及方法調用順序。
這裏NSKVONotifying_Person重寫class方法是爲了隱藏NSKVONotifying_Person。咱們在p1添加KVO後,打印p一、p2對象的class能夠發現他們都返回Person。
NSLog(@"%@, %@", [p1 class], [p2 class]);
// 打印結果: Person, Person
複製代碼
猜想NSKVONotifying_Person內重寫的class內部實現大體爲:
- (Class) class {
// 獲得類對象,在找到類對象父類
return class_getSuperclass(object_getClass(self));
}
複製代碼
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
複製代碼
打印結果:
didChangeValueForKey
方法內部調用了
observeValueForKeyPath:ofObject:change:context:
方法。