小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
如何手动触发KVO?
KVO的全称是Key-Value Observing, 俗称“键值监听”,可以用于监听某个对象属性值的改变。
KVO监听基本使用。例子代码:
创建一个Person类
@interface Person : NSObject
@property(nonatomic, assign) int age;
@end
@implementation Person
@end
复制代码
在控制器中使用创建person对象,并在点击屏幕的时候, 改变person的age 属性的值。
#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *person;
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
person.age = 9;
self.person = person;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.age = 10;
}
复制代码
如果我们想在person的age属性值,发生变化的时候。通知控制器做一些相应的处理。也就是说我们找一个监听者,监听person 的 age属性值的变化。该怎么实现。
通常的做法是:
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
复制代码
并在监听者的类中写下回调方法:
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 -%@",object, keyPath, change);
}
复制代码
注意:在不使用监听的时候,要销毁:
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
复制代码
这个时候,我们给person 新增一个属性,并让控制器监听新增的属性。比如height。这时候,我们就可以增加监听就好。
1. @property(nonatomic, assign) int height;
2.
Person *person = [[Person alloc] init];
person.age = 9;
person.height = 11;
self.person = person;
3.
[self.person addObserver:self forKeyPath:@"height" options:options context:nil];
4.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.age = 10;
self.person.height = 50;
}
复制代码
这个时候,我们点击屏幕,检测到调用两次KVO
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 -%@",object, keyPath, change);
}
复制代码
context:(void *)context 这个参数我们之前传递的nil
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
复制代码
如果我们修改一下代码:
[self.person addObserver:self forKeyPath:@"age" options:options context:@"333"];
[self.person addObserver:self forKeyPath:@"height" options:options context:@"444"];
复制代码
当我们监听到context方法参数 会带过来。
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 -%@-context:%@",object, keyPath, change, context);
}
复制代码
到此为止,KVO的基本用法,就这些。接下来,说一下KVO本质 我们创建person2。并在点击屏幕时,改变person2的属性值,但是我们不监听 person2的属性变化。
Person *person2 = [[Person alloc] init];
self.person2 = person2;
self.person2.age = 15;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.age = 10;
self.person2.age = 10;
}
复制代码
重写Person的setAge,setHeight方法。打断点监听发现。
@implementation Person
- (void)setAge:(int)age
{
NSLog(@"%d",age);
_age = age;
}
@end
复制代码
我们发现 person2 person 在对age属性赋值的时候,都调用了setAge:方法。
但是监听到了person对象age属性改变时的observeValueForKeyPath:监听回调。 由于person2的age属性没有被监听,所以不会调用observeValueForKeyPath。。。。。。
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 -%@",object, keyPath, change);
}
复制代码
为什么监听到了person属性的改变
没有监听到person2属性的改变呢?
我们发现他们改变age属性的时候,都是通过调用setAge方法改变的。
可是怎么就一个通过KVO监听到了,另一个
没有监听到呢?
本质是究竟是什么呢?
问题不是出现在setAge方法上, 因为大家调用的方法都是一样的。
问题可能出现在person对象上。
我打断点发现:
person的isa指针指向了NSKVONotifying_Person
(lldb) p self.person.isa**
(Class) $1 = NSKVONotifying_Person
Fix-it applied, fixed expression was:
self.person->isa
person2的isa指针指向了Person:
(lldb) p self.person2.isa**
(Class) $2 = Person
Fix-it applied, fixed expression was:
self.person2->isa
复制代码
前边的文章中,我们说过isa指向实例对象的类对象。我们发现
person,person2 的isa指针所指向的类对象不一样。
同样的类创建出来的对象,类对象为什么不一样了呢?
我们可以猜测,应该我们用KVO监听了person的属性变化, 没有用
KVO监听person2的属性变化。
未使用KVO监听的对象:
person的isa指针指向了Person类对象。
person2的isa指向了NSKVONotifying_Person类对象。
NSKVONotifying_Person是使用Runtime动态创建的一个类,是Person的子类。
NSKVONotifying_Person类的伪代码:
@interface NSKVONotifying_Person : Person
@end
复制代码
@implementation NSKVONotifying_Person
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
复制代码
这就是为什么person会收到 属性值改变时的observeValueForKeyPath:....调用。而person2不会收到属性值的改变。
内部执行逻辑验证:
写代码验证一下,
NSLog(@"person添加KVO之前-%@,%@",object_getClass(self.person),object_getClass(self.person2));
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之后-%@,%@",object_getClass(self.person),object_getClass(self.person2));
复制代码
nstance对象添加KVO之前的类对象。
instance对象添加KVO之后的类对象。
**2018-06-09 17:35:18.186795+0800 KVO****原理** **[2701:80467] person****添加****KVO****之前****-Person,Person**
**2018-06-09 17:35:21.410571+0800 KVO****原理** **[2701:80467] person****添加****KVO****之后****-NSKVONotifying_Person,Person**
**person 添加KVO监听之后,从Person变为**NSKVONotifying_Person****
**测试一下person添加KVO之后setAge方法的实现地址是否改变。**
复制代码
NSLog(@"person添加KVO之前-%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person添加KVO之后-%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
复制代码
**2018-06-09 17:47:27.560033+0800 KVO****原理** **[2937:100797] person****添加****KVO****之前****-0x10f95c570,0x10f95c570**
**2018-06-09 17:47:29.195660+0800 KVO****原理** **[2937:100797] person****添加****KVO****之后****-0x10fd01f8e,0x10f95c570**
**实现的地址改变了。但是实现到底是什么呢?怎么查看方法的具体实现?**
**2018-06-09 17:50:09.292854+0800 KVO****原理** **[3027:107515] person****添加****KVO****之前****-0x1005b0570,0x1005b0570**
**2018-06-09 17:50:11.988789+0800 KVO****原理** **[3027:107515] person****添加****KVO****之后****-0x100955f8e,0x1005b0570**
**(lldb) p (IMP) 0x1005b0570**
(IMP) $0 = 0x00000001005b0570 (KVO原理`-[Person setAge:] at Person.m:12)
**(lldb) p (IMP) 0x100955f8e**
(IMP) $1 = 0x0000000100955f8e (Foundation`_NSSetIntValueAndNotify)
**\
**我们发现,person添加KVO之后,setAge:方法的实现,是在Foundation模块中的 _NSSetIntValueAndNotify。 由于Foundation模块是不开源的,我们目前不能知道其中的源码。
如果你有一台越狱手机。并且有破解经验,你可以查看Foundation编译后的文件。利用反编译工具,查看汇编代码。
复制代码
_NSSetIntValueAndNotify()确实是存在于Foundation框架中。**
**并且,你会发现:**
**不至有_NSSetIntValueAndNotify**
**而且会有_NSSetDoubleValueAndNotify**
**_NSSetObjectValueAndNotify**\
**_NSSetFloatValueAndNotify**
**等等,主要看你的属性的类型是什么。底层就调用不同的方法。**
**_NSSet*ValueAndNotify内部实现是怎么样的呢?**
**大概格式就是:**
**[self willChangeValueForkey:@"age"];**
**// 原来的setter实现**
****
**[self didChangeValueForkey:@"age"];**
****didChangeValueForkey****
**内部会调用observer 的 observerValueForKeyPath:ofObject:**\
**change:context:方法。**
复制代码
可以在person中重写
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key ];
NSLog("willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog("didChangeValueForKey- start");
// 【super didChangeValueForKey】 内部会调用
**observerValueForKeyPath:ofObject:**\
**change:context:方法。**
[super didChangeValueForKey:key ];
NSLog("didChangeValueForKey - end");
}
来测试。
另外NSKVONotifying_Person类
除了重写了setAge:方法,还重写了
class方法和dealloc方法,及_isKVO
方法。
复制代码
@implementation NSKVONotifying_Person
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 屏幕内部实现,隐藏了NSKVONotifying_Person类的存在
- (Class)class
{
return [Person class];
}
- (void)dealloc
{
// 收尾工作
}
- (BOOL)_isKVOA
{
return YES;
}
@end
复制代码
怎么证明,
NSKVONotifying_Person类
除了重写了setAge:方法,还重写了
class方法和dealloc方法,及_isKVO
方法???
运用运行时,runtime来验证。
复制代码
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
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);
}
复制代码
[self printMethodNamesOfClass:object_getClass(self.person)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
复制代码
打印结果:
**NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,**
**Person setAge:, age,**
iOS用什么方式实现对一个对象的KVO?
(KVO的本质是什么?)
利用RuntimeAPI 动态生成一个子类,
并且让instance对象的isa指向这个全新的子类。
当修改instance对象的属性时,会
调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Obsever)的
监听方法(obseverValueForKeyPath:ofObject:change:context:)
如何手动触发KVO?
KVO一般是自动触发的,就算没有人去修改Person对象的属性
我也想去执行 触发逻辑,直接调用下面的方法。
willChangeValueForKey:
didChangeValueForKey:
直接修改成员变量会触发KVO吗?
不会触发KVO.
复制代码