KVO
(Key-Value Observing),也就是咱們常說的鍵值監聽
,能夠用於監聽某個對象屬性值的改變。KVO
使用比較簡單,以下所示定義了一個含有2個屬性的Student
類,而後聲明一個實例對象,並添加一個觀察者監聽某個屬性,當被監聽的屬性發生變化時就會調用觀察者的observeValueForKeyPath: ofObject: change: context:
方法。當不須要監聽的時候須要移除觀察者。bash
// Student.h文件
@interface Student : NSObject
@property (nonatomic , strong) NSString *name;
@property (nonatomic , strong) NSMutableArray booksArr;
@end
複製代碼
// 使用Student類的文件
- (void)test{
self.stu1 = [[Student alloc] init];
// 添加觀察者監聽name的變化
[self.stu1 addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:NULL];
NSLog(@"name改變前");
self.stu1.name = @"Jack";
NSLog(@"name改變後");
}
// 當監聽屬性發生變化時的回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"keyPath:%@,change-->%@",keyPath,change);
}
- (void)dealloc{
// 移除觀察者
[self.stu1 removeObserver:self forKeyPath:@"name"];
}
// ********************打印結果********************
2020-01-05 09:42:32.371008+0800 GCDDemo[13375:567451] name改變前
2020-01-05 09:42:32.371618+0800 GCDDemo[13375:567451] keyPath:name,change-->{
kind = 1;
new = Jack;
old = "<null>";
}
2020-01-05 09:42:32.371895+0800 GCDDemo[13375:567451] name改變後
複製代碼
KVO
的實現過程其實是利用了OC的runtime
機制,當一個實例對象(好比上面的self.stu1
)添加觀察者時,底層根據該實例對象所屬的類動態添加了一個類(動態添加的類名就是在原來類的類名前加上NSKVONotifying_
前綴),這個類是繼承自原來的類的。上面實例的底層實現過程以下:框架
self.stu1
添加觀察者時,底層就利用runtime
動態生成一個叫NSKVONotifying_Student
的類,這個類繼承自Student
類,並重寫了如下實例方法:
class
方法,不重寫的話調用這個方法返回的是NSKVONotifying_Student
這個類,重寫後返回的是本來的Student
類。蘋果這麼作的目的是爲了隱藏KVO
的實現細節。dealloc
方法,在這個方法裏面作一些收尾的工做。_isKVOA
方法,這是一個私有方法,咱們沒必要關心。setter方法
,上面案例只監聽了name
屬性,因此只需重寫setName:
方法。重寫setter是實現KVO
的關鍵,在setter方法裏面實際是調用的Foundation
框架下的_NSSet***ValueAndNotify
方法(***表示不是一個固定的,這個和監聽的屬性的類型有關,好比是屬性是int類型的話這裏就是__NSSetIntValueAndNotify,所包含的類型會在後面列出來)。self.stu1
這個實例對象的isa
改成指向NSKVONotifying_Student
(本來是指向Student
類的)。self.stu1.name = @"Jack"
,是調用的setName:
方法,前面說了setName:
方法被重寫了,因此實際上調用的是_NSSetObjectValueAndNotify
這個方法。這個方法實現蘋果是沒有開源的,沒法得知其具體實現,不過能夠猜出其實現流程大體以下:
[self willChangeValueForKey:@"name"];
這個方法。_name = name;
);[self didChangeValueForKey:@"name"];
這個方法。didChangeValueForKey:
這個方法中調用觀察者的observeValueForKeyPath: ofObject: change: context:
方法來通知觀察者屬性值發生了變化。Foundation
框架下的_NSSet***ValueAndNotify
系列方法列表以下:函數
_NSSetBoolValueAndNotify
_NSSetCharValueAndNotify
_NSSetDoubleValueAndNotify
_NSSetFloatValueAndNotify
_NSSetIntValueAndNotify
_NSSetLongLongValueAndNotify
_NSSetLongValueAndNotify
_NSSetObjectValueAndNotify
_NSSetPointValueAndNotify
_NSSetRangeValueAndNotify
_NSSetRectValueAndNotify
_NSSetShortValueAndNotify
_NSSetSizeValueAndNotify
_NSSetUnsignedCharValueAndNotify
_NSSetUnsignedIntValueAndNotify
_NSSetUnsignedLongLongValueAndNotify
_NSSetUnsignedLongValueAndNotify
_NSSetUnsignedShortValueAndNotify
複製代碼
這個其實咱們只須要打印一下再添加觀察者以前和以後實例對象所屬的類就知道了。不過前面已經說過了,動態添加的類重寫了class
方法,因此咱們不能經過這個方法來獲取一個實例對象的類,而要經過runtime
的object_getClass()
這個API來獲取:ui
- (void)test1{
self.stu1 = [[Student alloc] init];
NSLog(@"觀察前- [self.stu1 class] -->%@",[self.stu1 class]);
NSLog(@"觀察前- object_getClass(self.stu1) -->%@",object_getClass(self.stu1));
[self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
NSLog(@"觀察後- [self.stu1 class] -->%@",[self.stu1 class]);
NSLog(@"觀察後- object_getClass(self.stu1) -->%@",object_getClass(self.stu1));
}
// ********************打印結果********************
2020-01-05 10:51:00.584299+0800 GCDDemo[14497:600230] 觀察前- [self.stu1 class] -->Student
2020-01-05 10:51:00.584690+0800 GCDDemo[14497:600230] 觀察前- object_getClass(self.stu1) -->Student
2020-01-05 10:51:00.592797+0800 GCDDemo[14497:600230] 觀察後- [self.stu1 class] -->Student
2020-01-05 10:51:00.593064+0800 GCDDemo[14497:600230] 觀察後- object_getClass(self.stu1) -->NSKVONotifying_Student
複製代碼
這裏咱們須要用到runtime
的一些API來獲取一個類對象裏面存儲的方法列表信息,下面咱們先封裝一個方法來獲取這些信息,而後把監聽前和監聽後的方法列表打印出來。atom
- (void)test2{
self.stu1 = [[Student alloc] init];
NSLog(@"觀察前方法列表-->%@",[self methodNamesOfClass:object_getClass(self.stu1)]);
[self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
NSLog(@"觀察後方法列表-->%@",[self methodNamesOfClass:object_getClass(self.stu1)]);
}
// 傳入一個類,將類中方法列表的方法名拼接換成字符串返回
- (NSString *)methodNamesOfClass:(Class)cls{
unsigned int count;
// 獲取方法列表
Method *methodList = class_copyMethodList(cls, &count);
NSString *methodNamesStr = @"";
// 遍歷方法列表將方法名拼接成字符串
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
methodNamesStr = [methodNamesStr stringByAppendingFormat:@"%@ ,",methodName];
}
// 釋放
free(methodList);
return methodNamesStr;
}
// ********************打印結果********************
2020-01-05 10:56:43.077817+0800 GCDDemo[14606:603376] 觀察前方法列表-->.cxx_destruct ,name ,setName: ,age ,setAge: ,
2020-01-05 10:56:43.078483+0800 GCDDemo[14606:603376] 觀察後方法列表-->setName: ,class ,dealloc ,_isKVOA ,
複製代碼
這裏咱們一樣須要用到runtime
的API,首先經過class_getInstanceMethod()
函數來獲取setter方法的Method
,而後再調用method_getImplementation()
來獲得setter方法的IMP
。spa
不過咱們首先打印的是IMP
的地址,想要看IMP
的具體信息咱們須要打一個斷點調出LLDB
,而後藉助LLDB
來打印具體信息。好比在監聽前的IMP
地址是0x10967d4c0
,就能夠在LLDB
中輸入p (IMP)0x10967d4c0
來打印具體信息。從下面能夠看出監聽前setter方法就是正常的,監聽後就變成了_NSSetObjectValueAndNotify
。code
- (void)test1{
self.stu1 = [[Student alloc] init];
NSLog(@"監聽前的setter方法IMP-->%p",[self IMPWithSelector:@selector(setName:)]);
[self.stu1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
NSLog(@"監聽後的setter方法IMP-->%p",[self IMPWithSelector:@selector(setName:)]);
}
// 獲取一個方法的IMP
- (IMP)IMPWithSelector:(SEL)selector{
Class cls = object_getClass(self.stu1);
Method methon = class_getInstanceMethod(cls, selector);
IMP imp = method_getImplementation(methon);
return imp;
}
// ********************打印結果********************
2020-01-05 11:25:40.485792+0800 GCDDemo[15032:617260] 監聽前的setter方法IMP-->0x10967d4c0
2020-01-05 11:25:40.489656+0800 GCDDemo[15032:617260] 監聽後的setter方法IMP-->0x7fff25701c8a
(lldb) p (IMP)0x10967d4c0
(IMP) $0 = 0x000000010967d4c0 (GCDDemo`-[Student setName:] at Student.h:15)
(lldb) p (IMP)0x7fff25701c8a
(IMP) $1 = 0x00007fff25701c8a (Foundation`_NSSetObjectValueAndNotify)
複製代碼
KVO
的核心是動態生成一個繼承自原類的類,而後將實例對象的isa
指向這個類。而後重寫了監聽屬性的setter方法,在原有setter方法的前面調用willChangeValueForKey
方法,在原有setter方法的後面調用didChangeValueForKey
。orm
因此咱們要判斷某個操做是否會觸發KVO
關鍵在於它是否調用了監聽屬性的setter方法。好比上面的例子,self.stu1.name = @"Jack";
這種方式就是調用setter方法,因此它會觸發KVO
。可是下面這幾種方式是不會觸發KVO
的:server
self.stu1->_name = @"Jack";
(前提是須要將成員變量_name給暴露出去才能在外面訪問),這種方式是不會觸發KVO
的,由於它沒有調用setter方法。KVO
的。好比[self.stu1.booksArr addObject:@"book1"]
這樣的操做,它一樣沒有調用setBooksArr:
方法,因此不會觸發KVO
。Dog
類裏面有個age
屬性,Student
類裏面有個Dog
類型的屬性dog
,若是咱們監聽dog
這個屬性,當dog
的age
發生變化時並不會觸發KVO
,由於它不會調用setDog:
方法。上面這幾種狀況,若是咱們也想觸發KVO
的話,咱們能夠手動觸發,也就是在原有方法的前面和後面分別加上willChangeValueForKey
和didChangeValueForKey
這兩個方法。就好比最後這個例子,咱們能夠這樣寫:對象
[self.stu1 willChangeValueForKey:@"dog"];
self.stu1.dog.age = 3;
[self.stu1 didChangeValueForKey:@"dog"];
複製代碼
最後還有一點要說明,經過KVC方式設置屬性值也是會觸發KVO的
。好比[self.stu1 setValue:@"Jack" forKey:@"name"];
這樣寫是能夠觸發KVO
的,這應該是蘋果在KVC
實現中調用了willChangeValueForKey
和didChangeValueForKey
這兩個方法。