面試題引發(fā)的思考:
Q: iOS用什么方式實(shí)現(xiàn)對一個對象的KVO苏携?即KVO的本質(zhì)是什么羞反?
利用RuntimeAPI動態(tài)生成一個子類,并且讓instance對象的
isa
指向這個全新的子類。當(dāng)修改instance對象的屬性時,會調(diào)用
Foundation
的_NSSetXXXValueAndNotify
函數(shù)
a>willChangeValueForKey:
b>父類原來的setter
方法對成員變量進(jìn)行賦值
c>didChangeValueForKey:
d>內(nèi)部會觸發(fā)監(jiān)聽器(observer
)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
Q: 如何手動觸發(fā)KVO丹诀?
- 手動調(diào)用
willChangeValueForKey:
和didChangeValueForKey:
。
Q: 直接修改成員變量會觸發(fā)KVO嗎翁垂?
- 不會觸發(fā)KVO铆遭。
1. KVO介紹
KVO的全稱是Key-Value Observing,即“鍵值監(jiān)聽”沿猜,用于監(jiān)聽某個對象屬性值的改變
- (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對象添加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
對象添加KVO監(jiān)聽后,age
屬性的值發(fā)生改變時啼肩,監(jiān)聽者observeValueForKeyPath
的方法會被調(diào)用執(zhí)行橄妆。
2. KVO的本質(zhì)
(1)未使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程
給person1
和person2
的屬性age
賦值,都會調(diào)用相同的set
方法祈坠,而set
方法的實(shí)現(xiàn)也是一樣的害碾。
Q: 那為什么只會打印出person1
的相關(guān)內(nèi)容?
可以猜測是跟類的對象方法沒有關(guān)系赦拘,跟類對象本身有關(guān)慌随。
我們知道instance對象的isa
指向class對象,所以:
person1
的類對象是NSKVONotifying_Person
躺同,person2
的類對象是Person
阁猜;- 而
NSKVONotifying_Person
則是使用Runtime動態(tài)創(chuàng)建的一個類,是Person
的一個子類蹋艺。
由上圖可知:
person2
在調(diào)用setAge:
方法的時候剃袍,首先根據(jù)person2
的isa
找到Person
的class對象,然后在class對象中找到setAge:
捎谨,然后實(shí)現(xiàn)方法民效。這是未使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程。
(2) 使用KVO監(jiān)聽的對象實(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:
方法對成員變量進(jìn)行賦值- 最后調(diào)用
didChangeValueForKey:
方法,此方法內(nèi)部會觸發(fā)監(jiān)聽器(Oberser
)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
由上圖可知:
person1
在調(diào)用setAge:
方法的時候州叠,首先根據(jù)person1
的isa
找到NSKVONotifying_Person
的class對象棵红,然后在class對象中找到setAge:
,然后實(shí)現(xiàn)方法咧栗。這是使用了KVO監(jiān)聽的對象實(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對象添加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);
// 存儲方法名
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個對象方法,分別為 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對象添加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
會重寫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對象添加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
這個類暴露出來异逐,不希望開發(fā)者知道其內(nèi)部實(shí)現(xiàn),其class
方法內(nèi)部實(shí)現(xiàn)應(yīng)該是以下:
// 屏蔽內(nèi)部實(shí)現(xiàn)插掂,隱藏了NSKVONotifying_Person類的存在
- (Class)class {
// 1.獲取類對象 2.獲取類對象父類
return class_getSuperclass(object_getClass(self));
}
4> 驗(yàn)證4:didChangeValueForKey:
內(nèi)部會觸發(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對象添加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)部會調(diào)用observer
的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
。