面試題引發(fā)的思考:
Q: iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?即KVO的本質(zhì)是什么?
利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類,并且讓instance對(duì)象的
isa指向這個(gè)全新的子類。當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)調(diào)用
Foundation的_NSSetXXXValueAndNotify函數(shù)
a>willChangeValueForKey:
b>父類原來的setter方法對(duì)成員變量進(jìn)行賦值
c>didChangeValueForKey:
d>內(nèi)部會(huì)觸發(fā)監(jiān)聽器(observer)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
Q: 如何手動(dòng)觸發(fā)KVO?
- 手動(dòng)調(diào)用
willChangeValueForKey:和didChangeValueForKey:。
Q: 直接修改成員變量會(huì)觸發(fā)KVO嗎?
- 不會(huì)觸發(fā)KVO。
1. KVO介紹
KVO的全稱是Key-Value Observing,即“鍵值監(jiān)聽”,用于監(jiān)聽某個(gè)對(duì)象屬性值的改變
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
person1.age = 10;
Person *person2 = [[Person alloc] init];
person2.age = 20;
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
person1.age = 20; // [person1 setAge:20];
person2.age = 30; // [person2 setAge:30];
[person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變 - %@", object, keyPath, change);
}
// 打印結(jié)果
Demo[1234:567890] 監(jiān)聽到<Person: 0x600001608200>的age屬性發(fā)生了改變 - {
kind = 1;
new = 20;
old = 10;
}
由打印結(jié)果可知:
輸出的是person1的相關(guān)內(nèi)容;給person1對(duì)象添加KVO監(jiān)聽后,age屬性的值發(fā)生改變時(shí),監(jiān)聽者observeValueForKeyPath的方法會(huì)被調(diào)用執(zhí)行。
2. KVO的本質(zhì)
(1)未使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程
給person1和person2的屬性age賦值,都會(huì)調(diào)用相同的set方法,而set方法的實(shí)現(xiàn)也是一樣的。
Q: 那為什么只會(huì)打印出person1的相關(guān)內(nèi)容?
可以猜測(cè)是跟類的對(duì)象方法沒有關(guān)系,跟類對(duì)象本身有關(guān)。

我們知道instance對(duì)象的isa指向class對(duì)象,所以:
person1的類對(duì)象是NSKVONotifying_Person,person2的類對(duì)象是Person;- 而
NSKVONotifying_Person則是使用Runtime動(dòng)態(tài)創(chuàng)建的一個(gè)類,是Person的一個(gè)子類。

由上圖可知:
person2在調(diào)用setAge:方法的時(shí)候,首先根據(jù)person2的isa找到Person的class對(duì)象,然后在class對(duì)象中找到setAge:,然后實(shí)現(xiàn)方法。這是未使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程。
(2) 使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程
根據(jù)相關(guān)資料可知:
NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify,其內(nèi)部實(shí)現(xiàn)流程為:
- 首先調(diào)用
willChangeValueForKey:方法- 然后調(diào)用父類的
setAge:方法對(duì)成員變量進(jìn)行賦值- 最后調(diào)用
didChangeValueForKey:方法,此方法內(nèi)部會(huì)觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

由上圖可知:
person1在調(diào)用setAge:方法的時(shí)候,首先根據(jù)person1的isa找到NSKVONotifying_Person的class對(duì)象,然后在class對(duì)象中找到setAge:,然后實(shí)現(xiàn)方法。這是使用了KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程。
1> 驗(yàn)證1:NSKVONotifying_Person的內(nèi)部結(jié)構(gòu)
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethodNamesOfClass:object_getClass(person2)];
[self printMethodNamesOfClass:object_getClass(person1)];
}
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲(chǔ)方法名
NSMutableString *methodNames = [NSMutableString string];
for (int i=0; i<count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
NSLog(@"%@ - %@", cls, methodNames);
}
// 打印結(jié)果
Demo[1234:567890] Person - age, setAge:,
Demo[1234:567890] NSKVONotifying_Person - setAge:, class, dealloc, _isKVOA,
由打印結(jié)果可知:
NSKVONotifying_Person中有4個(gè)對(duì)象方法,分別為 setAge:、class、dealloc、_isKVOA;證實(shí)了其內(nèi)部結(jié)構(gòu)。
2> 驗(yàn)證2:NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
NSLog(@"person1添加KVO監(jiān)聽之前 - %p %p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加KVO監(jiān)聽之后 - %p %p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
[person1 removeObserver:self forKeyPath:@"age"];
}

由打印結(jié)果可知:
person1添加KVO監(jiān)聽之前,person1和person2的setAge:方法的地址相同;person1添加KVO監(jiān)聽之后,person1的setAge:方法的地址發(fā)生改變。
打印結(jié)果證實(shí)了:
NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify。
內(nèi)部實(shí)現(xiàn)偽代碼如下:
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
// 偽代碼
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 通知監(jiān)聽器,某某屬性值發(fā)生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
3> 驗(yàn)證3:NSKVONotifying_Person會(huì)重寫class方法
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"%@ %@", [person1 class], [person2 class]);
NSLog(@"%@ %@", object_getClass(person1), object_getClass(person2));
[person1 removeObserver:self forKeyPath:@"age"];
}
// 打印結(jié)果
Demo[1234:567890] Person Person
Demo[1234:567890] NSKVONotifying_Person Person
由打印結(jié)果可知:
通過class方法獲取到的類為Person,而通過runtime的object_getClass方法獲取到的類為NSKVONotifying_Person,說明NSKVONotifying_Person重寫了class方法
Q: 那為什么要重寫class方法呢?
很明顯,蘋果不想讓NSKVONotifying_Person這個(gè)類暴露出來,不希望開發(fā)者知道其內(nèi)部實(shí)現(xiàn),其class方法內(nèi)部實(shí)現(xiàn)應(yīng)該是以下:
// 屏蔽內(nèi)部實(shí)現(xiàn),隱藏了NSKVONotifying_Person類的存在
- (Class)class {
// 1.獲取類對(duì)象 2.獲取類對(duì)象父類
return class_getSuperclass(object_getClass(self));
}
4> 驗(yàn)證4:didChangeValueForKey:內(nèi)部會(huì)觸發(fā)observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
在Person類中,重寫willChangeValueForKey:和didChangeValueForKey:方法:
// TODO: ----------------- Person類 -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge:");
}
- (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");
}
@end
// TODO: ----------------- ViewController類 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person = [[Person alloc] init];
person.age = 10;
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person addObserver:self forKeyPath:@"age" options:options context:nil];
person.age = 20;
[person removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變 - %@", object, keyPath, change);
}

由打印結(jié)果可知:
didChangeValueForKey:內(nèi)部會(huì)調(diào)用observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:。