KVO:
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变.
self.person = [[LDPerson alloc] init];
self.person.age = 10;
//添加键值监听
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//在touchesBegan方法中修改age属性值.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;
//就会在下面的方法中监听到变化的具体信息,哪个对象的哪个属性,变化前后的属性值.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
}
//NSLog信息:
监听到了<LDPerson: 0x60c00000fab0>对象的age属性由{
kind = 1;
new = 20;
old = 10;
}变成了{
kind = 1;
new = 20;
old = 10;
}属性
//需要在不使用的时候,移除监听
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO原理:
通过修改instance
对象的isa
指针,指向一个通过runtime
动态生成的子类,例如self.person
的isa
指针并不是指向LDPerson
的class
对象,而是指向NSKVONotifying_LDPerson
,该类是LDPerson
的一个子类.
未使用KVO监听的对象:
使用了KVO监听的对象:
image.png
_NSSet*ValueAndNotify的内部实现
- (void)setAge:(int)age{
_NSSet*ValueAndNotify();
}
void _NSSet*ValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//通过监听器,监听属性发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
image.png
1.调用willChangeValueForKey:
2.调用原来的setter实现
3.调用didChangeValueForKey:
- didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法
代码验证上述流程
//LDPerson内部代码实现
- (void)setAge:(int)age{
_age = age;
NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey == begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey == end");
}
通过打印信息可以看到,流程如上所述:
image.png
通过runtime
查看isa
指针指向的 class
对象
可以看到添加KVO之后,instacne
对象的isa
所指向的class
对象发生了变化.
通过LLDB
查看- (void)setAge:(int)age
方法.
- 如果
LDPerson
实现了setAge
会提示(KVO-[LDPerson setAge:] at LDPerson.m:12)
setAge在.m文件当中第12行. - 如果
LDPerson
只是通过@property
则会提示在.h文件中. - 可以观察到添加了KVO属性的
setter
实现不同.添加之前是在LDPerson
文件中,添加KVO之后setter
实现是在Foundation
框架中,_NSSet*ValueAndNotify
-
runtime
动态生成的类NSKVONotifying_LDPerson
,其isa
指向该类的元类对象NSKVONotifying_LDPerson
,其superClass指针指向
LDPerson,即生成的class对象有自己的
meta-class对象.其父类为添加KVO对象的class对象
LDPerson`.
image.png
动态生成的NSKVONotifying_LDPerson类
runtime
动态生成的这个类内部有几个特殊的方法,并重写了setAge
,class
,dealloc
方法
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏幕内部实现,隐藏了NSKVONotifying_LDPerson类的存在
- (Class)class
{
return [LDPerson class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
为何重写class
方法:
屏幕内部实现,隐藏了NSKVONotifying_LDPerson
类的存在.从开发者的使用角度来看,并不知道NSKVONotifying_LDPerson
类的存在,如果没有重写class
方法,当该对象调用class
方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class
方法是NSObject
中的方法,如果不重写最终可能会返回NSKVONotifying_LDPerson
,就会将该类暴露出来,也给开发者造成困扰,写的是LDPerson
,添加KVO之后class
方法返回会改变.
猜测NSObject
的class
方法的实现:
self
为添加了KVO后的LDPerson
,最红返回可能是NSKVONotifying_LDPerson
.
- (Class)class
{
return object_getClass(self);
}
通过runtime 检验class
,dealloc``isKVOA
方法的存在
- (void)printMethodNameOfClass:(Class)cls{
unsigned int count;
//获取方法列表数组
Method * methodList = class_copyMethodList(cls, &count);
//存储方法名
NSMutableString * methodsNames = [NSMutableString string];
//遍历方法数组
for (int i = 0; i < count; i++) {
//获取方法
Method method = methodList[I];
//获取方法名字
NSString * methodName = NSStringFromSelector(method_getName(method));
//拼接方法
[methodsNames appendString:methodName];
[methodsNames appendString:@", "];
}
//释放 如果C语言的数据结构是是通过copy
//creat 等创建出来的,一般都需要释放
free(methodList);
//打印方法名
NSLog(@"%@ %@",cls,methodsNames);
}
self.person1 = [[LDPerson alloc] init];
self.person1.age = 10;
self.person2 = [[LDPerson alloc] init];
self.person2.age = 20;
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//person1 添加了KVO
[self printMethodNameOfClass:object_getClass(self.person1)];
//person2 没有添加了KVO
[self printMethodNameOfClass:object_getClass(self.person2)];
可以看到:
- 添加了KVO的
NSKVONotifying_LDPerson
类的内部有class
,dealloc``isKVOA
和setter
方法.没有age
的getter
方法.因为NSKVONotifying_LDPerson
类只会重写setter
方法,该setter
方法内部会调用willChangeValueForKey
,didChangeValueForKey
来实现KVO监听.而getter
方法存在NSKVONotifying_LDPerson
类的父类LDPerson
中. - 没有添加KVO的
LDPerson
只有一个property
属性,还属性会生成setter
和getter
方法.
image.png
iOS用什么方式实现对一个对象的KVO?(KVO本质是什么?)
- 利用
runtime
API动态生成一个子类,并让instance
对象的isa
指向这个全新的子类. - 当修改
instance
对象的属性时,会调用Foundation
的_NSSetxxxValueAndNotify
函数该函数会调用以下函数:willChangeValueForKey
- 父类原来的
setter
方法 -
didChangeValueForKey
该函数内部会出发监听器(Oberser)的监听方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
如何手动触发KVO
解读:添加KVO之后正常情况下触发KVO的条件是:修改了KVO监听的属性值,此时会触发KVO监听方法.此题的意思是在没有修改KVO监听的属性值时,如果触发KVO监听方法?
思路:动态生成的子类NSKVONotifying_LDPerson
内部主要的操作是重写了setter
方法,方法内部调用了
willChangeValueForKey
,didChangeValueForKey
方法.
所以:
我们只需要被监听的instance
对象手动调用两个方法即可:
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
哪些对象可以使用KVO监听?核心是重写了setter
,如果有setter
就可以实现监听,如果没有setter
就无法使用KVO监听.
直接修改成员变量会触发KVO吗
不会,没有在setter
中调用willChangeValueForKey, didChangeValueForKey
核心方法.
KVC
KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性
常见的API有:
-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
setValue:(id)value forKey和setValue:(id)value forKeyPath有什么不同?
value forKeyPath
会通过路径向下级继续查找并赋值
value forKey
只会在当前对象的路径下赋值,不会继续查找
@interface LDCat : NSObject
@property (assign, nonatomic) int weight;
@end
@interface ldPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) LDCat *cat;
@end
[person valueForKey:@"age"];
[person setValue:@10 forKeyPath:@"cat.weight"];
通过KVC修改属性会触发KVO吗?(会)
解读:该题的意思是如下代码会不会出发KVO.
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[LDPerson alloc] init];
self.person1.age = 10;
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person1 setValue:@20 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到了%@对象的%@属性改变%@",object,keyPath,change);
}
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
KVC的核心是在`setter`中调用`willChangeValueForKey, didChangeValueForKey`方法,上图代码中不用`@property`关键字生成`setter方法,可以看到依然会触发KVO,可知KVC内部会自动调用上述`willChangeValueForKey, didChangeValueForKey`方法来触发KVO监听
KVC的赋值和取值过程是怎样的,原理是什么?
赋值
image.png解读:如果调用了
setValue: forKey:
方法
1.会首先查找setKey:
方法,如果没有setKey:
方法,接下来会查找_setKey:
方法.至此如果查找到两个方法中的热议一个,就会传递参数,调用方法,出发KVO.
2.如果上述两个方法都没有查找到.会查看accessInstanceVariablesDirectly
的返回值.该方法标识是都可以直接访问成员变量,如果返会NO
即不能直接访问成员变量,就会抛出异常,找不到合适的Key
赋值.如果可以直接访问成员变量,接下来就会查找属性名,按照如下顺序查找:_key _isKey key isKey
如果找到成员变量直接赋值,触发KVO.
KVC过程总结:
- 首先按照顺序查找
setter
方法
1.1:setKey:
--->_setKey:
2:如果没有查找到setter
方法,会查看accessInstanceVariablesDirectly(是否可以直接访问成员变量,默认返回YES)
函数返回值.如果返回值为NO
就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException
.如果返回值为YES
,
3: 接下来会进入查找成员变量,直接赋值的流程
查找顺序为:_Key
--> _isKey
---> key
--->isKey
4.如果没有查找到成员变量,就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException
valueForKey:取值过程
image.png1.查找
getter
方法getKey
---> Key
---> isKey
---> _key
找到该方法,调用并返回对应的值,如果没找到
2.查看
accessInstanceVariablesDirectly
方法返回值如果返回
YES
进入下个流程,如果返回NO
就会调用setValue:forUndefinedKey:
并抛出异常
NSUnknownKeyException
3.返回值为
YES
即可直接访问成员变量,就会俺书讯查找成员变量: _key
---> _iskey
---> key
---> iskey
4.如果查找到调用并返回对应的值.如果没找到就会调用
setValue:forUndefinedKey:
并抛出异常
NSUnknownKeyException
网友评论