面試題:
KVC相關(guān):
1\. 通過(guò)KVC修改屬性會(huì)觸發(fā)KVO么?直接修改成員變量呢 ?
2\. KVC的賦值和取值過(guò)程是怎樣的器净?原理是什么?
KVC的實(shí)現(xiàn)原理Demo
1. 什么是KVC袜炕?
KVC的全稱key - value - coding麻诀,俗稱"鍵值編碼",可以通過(guò)key來(lái)訪問(wèn)某個(gè)屬性
常見(jiàn)的API有:
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
簡(jiǎn)單的代碼實(shí)現(xiàn): DLPerson 和 DLCat
///> DLPersin.h 文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
DLCat
*/
@interface DLCat : NSObject
@property (nonatomic, assign) int weight;
@end
/**
DLPerson
*/
@interface DLPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, strong) DLCat *cat;
@end
NS_ASSUME_NONNULL_END
1.1 - (void)setValue:(id)value forKey:(NSString *)key;
///> ViewController.m 文件
- (void)viewDidLoad {
[super viewDidLoad];
DLPerson *person = [[DLPerson alloc]init];
[person setValue:@20 forKey:@"age"];
NSLog(@"%d",person.age);
}
1.1 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
///> ViewController.m 文件
- (void)viewDidLoad {
[super viewDidLoad];
DLPerson *person = [[DLPerson alloc]init];
person.cat = [[DLCat alloc]init];
[person setValue:@20 forKeyPath:@"cat.weight"];
NSLog(@"%d",person.age);
NSLog(@"%d",person.cat.weight);
}
setValue:(id)value forKeyPath:(NSString *)keyPath
和 setValue:(id)value forKey:(NSString *)key
的區(qū)別:
- keyPath 相當(dāng)于根據(jù)路徑去尋找屬性,一層一層往下找蠢莺,
- key 是直接哪去屬性的名字設(shè)置寒匙,如果按路徑找會(huì)報(bào)錯(cuò)
1.3 - (id)valueForKey:(NSString *)key;
///> ViewController.m 文件
- (void)viewDidLoad {
[super viewDidLoad];
DLPerson *person = [[DLPerson alloc]init];
person.age = 10;
NSLog(@"%@",[person valueForKey:@"age"]);
}
1.4 - (id)valueForKeyPath:(NSString *)keyPath;
///> ViewController.m 文件
- (void)viewDidLoad {
[super viewDidLoad];
DLPerson *person = [[DLPerson alloc]init];
person.age = 10;
NSLog(@"%@",[person valueForKeyPath:@"cat.weight"]);
}
(id)valueForKey:(NSString *)key;
和 (id)valueForKeyPath:(NSString *)keyPath
的區(qū)別:
- 同理1.2-1.3
2. setValue:forKey:的原理
當(dāng)我們?cè)O(shè)置setValue:forKey:時(shí)
首先會(huì)查找setKey:、_setKey: (按順序查找)
如果有直接調(diào)用
-
如果沒(méi)有躏将,先查看accessInstanceVariablesDirectly方法
+ (BOOL)accessInstanceVariablesDirectly{ return YES; ///> 可以直接訪問(wèn)成員變量 // return NO; ///> 不可以直接訪問(wèn)成員變量, ///> 直接訪問(wèn)會(huì)報(bào)NSUnkonwKeyException錯(cuò)誤 }
如果可以訪問(wèn)會(huì)按照 _key锄弱、_isKey、key祸憋、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤
_key会宪、_isKey、key蚯窥、iskey復(fù)制順序
///> DLPerson.h 文件
@interface DLPerson : NSObject{
@public
int _age;
int _isAge;
int age;
int isAge;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[DLPerson alloc]init];
///> person1添加kvo監(jiān)聽(tīng)
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
///> 通過(guò)KVC修改person.age的值
[self.person1 setValue:@20 forKey:@"age"];
NSLog(@"------");
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"監(jiān)聽(tīng)到了%@的%@屬性發(fā)生了改變%@",object,keyPath,change);
}
- (void)dealloc{
///> 使用結(jié)束后記得移除
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- 在NSLog(@"------");位置打下短點(diǎn)注釋方式去查看各個(gè)值得復(fù)制順序
- 然后在控制臺(tái)中查看復(fù)制查找的順序就OK了掸鹅,
- DLPerson文件中的順序即使是調(diào)換了也無(wú)所謂。
3. valueForKey:的原理
kvc取值按照 getKey拦赠、key巍沙、iskey、_key 順序查找方法
存在直接調(diào)用
-
沒(méi)找到同樣荷鼠,先查看accessInstanceVariablesDirectly方法
+ (BOOL)accessInstanceVariablesDirectly{ return YES; ///> 可以直接訪問(wèn)成員變量 // return NO; ///> 不可以直接訪問(wèn)成員變量, ///> 直接訪問(wèn)會(huì)報(bào)NSUnkonwKeyException錯(cuò)誤 }
如果可以訪問(wèn)會(huì)按照 _key句携、_isKey、key允乐、iskey的順序查找成員變量
找到直接復(fù)制
未找到報(bào)錯(cuò)NSUnkonwKeyException錯(cuò)誤
三. 知識(shí)點(diǎn)補(bǔ)充
1. _NSSetIntValueAndNotify();方法來(lái)源
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[DLPerson alloc]init];
self.person1.age = 10;
self.person2 = [[DLPerson alloc]init];
self.person2.age = 20;
///> person1添加kvo監(jiān)聽(tīng)
NSLog(@"person添加KVO之前 - person1:%@矮嫉, person2:%@",object_getClass(self.person1), object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之前 - person1:%@, person2:%@",object_getClass(self.person1), object_getClass(self.person2));
}
輸出結(jié)果:
person添加KVO之前 - person1:DLPerson牍疏, person2:DLPerson
person添加KVO之后 - person1:NSKVONotifying_DLPerson敞临, person2:DLPerson
由此可見(jiàn)在沒(méi)有 為person1添加KVO之前 person1.isa指針仍然是DLPerson
那么我們就可以使用- (IMP)methodForSelector:(SEL)aSelector;去查看實(shí)現(xiàn)方法的地址,的具體方法代碼如下:
///> person1添加kvo監(jiān)聽(tīng)
NSLog(@"person添加KVO之前 - person1:%p麸澜, person2:%p \n",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之后 - person1:%p挺尿, person2:%p \n",[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
}
輸出結(jié)果:
person添加KVO之前 - person1:0x10852a560, person2:0x10852a560
person添加KVO之后 - person1:0x108883fc2炊邦, person2:0x10852a560
由此可見(jiàn)编矾,在添加之前person1和person2實(shí)現(xiàn)的setAge方法是一個(gè),添加之后person1的setAge方法就有了變化馁害。
然后我們打入斷點(diǎn)去查看實(shí)現(xiàn)的方法:
在控制臺(tái)中使用 p (IMP)方法地址
來(lái)打印得到方法的名稱窄俏。 所以我們?cè)谔砑覭VO之后的 setAge:
方法調(diào)用了 _NSSetIntValueAndNotify()
。
如果定義的屬性是類型是double則調(diào)用的是_NSSetDoubleValueAndNotify()
大家可以自己測(cè)試一下碘菜。
此方法在Foundtion框架中有對(duì)應(yīng)的
NSSetDoubleValueAndNotify()
NSSetIntValueAndNotify()
NSSetCharValueAndNotify()
...
目前還未深入接觸到逆向工程凹蜈。等以后學(xué)到了在給大家詳解解釋吧限寞。
2. NSKVONotifying_DLPerson的isa指針指向哪里?
在 KVO的本質(zhì)分析 中我們得知仰坦,添加了KVO監(jiān)聽(tīng)的實(shí)例對(duì)象isa指針指向了NSKVONotifying_DLPerson類履植, 那么NSKVONotifying_DLPerson的isa指針的指向?
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[DLPerson alloc]init];
self.person1.age = 10;
self.person2 = [[DLPerson alloc]init];
self.person2.age = 20;
///> person1添加kvo監(jiān)聽(tīng)
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"類對(duì)象 - person1: %@<%p> person2: %@<%p>",
object_getClass(self.person1), ///> self.person1.isa 類名
object_getClass(self.person1), ///> self.person1.isa
object_getClass(self.person2), ///> self.person1.isa 類名
object_getClass(self.person2) ///> self.person1.isa
);
NSLog(@"元類對(duì)象 - person1: %@<%p> person2: %@<%p>",
object_getClass(object_getClass(self.person1)), ///> self.person1.isa.isa 類名
object_getClass(object_getClass(self.person1)), ///> self.person1.isa.isa
object_getClass(object_getClass(self.person2)), ///> self.person2.isa.isa 類名
object_getClass(object_getClass(self.person2)) ///> self.person2.isa.isa
);
}
輸出結(jié)果:
類對(duì)象 - person1: NSKVONotifying_DLPerson<0x6000002cef40> person2: DLPerson<0x1002c9048>
元類對(duì)象 - person1: NSKVONotifying_DLPerson<0x6000002cf210> person2: DLPerson<0x1002c9020>
結(jié)果發(fā)現(xiàn):每一個(gè)類對(duì)象的地址是不一樣的悄晃,而且元類對(duì)象的地址也不一樣的玫霎,所以我們可以認(rèn)為 NSKVONotifying_DLPerson類有自己的元類對(duì)象, NSKVONotifying_DLPerson.isa指向著自己的元類對(duì)象妈橄。
四. 面試題答案
-
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ù) willChangeValueForKey: 父類原來(lái)的setter didChangeValueForKey: 內(nèi)部會(huì)觸發(fā)監(jiān)聽(tīng)器(Oberser)的監(jiān)聽(tīng)方法(observeValueForKeyPath:ofObject:change:context:)
-
如何手動(dòng)觸發(fā)KVO?
手動(dòng)調(diào)用willChangeValueForKey:和didChangeValueForKey:
-
直接修改成員變量會(huì)觸發(fā)KVO么沙热?
不會(huì)觸發(fā)KVO普舆,因?yàn)橹苯有薷某蓡T變量并沒(méi)有走set方法。
KVC相關(guān):
-
通過(guò)KVC修改屬性會(huì)觸發(fā)KVO么校读?
會(huì)觸發(fā)KVO,如上流程圖
-
KVC的賦值和取值過(guò)程是怎樣的祖能?原理是什么歉秫?
如上流程圖
- 文章總結(jié)自MJ老師底層視頻。