面试题
- iOS用什么方法实现对一个对象的kvo,kvo的本质是什么?
答:KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
举例
我们用一个代码例子来总结原理,首先创建一个 MyPerson 类,里面只有一个 age 的属性:
@interface MyPerson : NSObject
@property (assign, nonatomic) int age;
@end
然后在外部控制器中我们用 MyPerson
声明两个示例对象 person1
和 person2
。只对 person1 添加 KOV 监听,然后通过打印监听前和监听后的内存地址,来看他们的 isa 指针指向是否有变化,并且打印 person1 和 person2 的类对象和元类对象进行对比,methodForSelector
用于打印出方法实现。
@interface ViewController ()
@property (strong, nonatomic) MyPerson *person1;
@property (strong, nonatomic) MyPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MyPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MyPerson alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO监听之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSLog(@"类对象 - %@ %@",
object_getClass(self.person1), // self.person1.isa
object_getClass(self.person2)); // self.person2.isa
NSLog(@"元类对象 - %@ %@",
object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// NSKVONotifying_MyPerson是使用Runtime动态创建的一个类,是MyPerson的子类
// self.person1.isa == NSKVONotifying_MyPerson
//[self.person1 setAge:21];
self.person1.age = 20;
self.person2.age = 20;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
打印结果:
person1添加KVO监听之前 - MyPerson MyPerson
person1添加KVO监听之前 - 0x1078b6440 0x1078b6440
person1添加KVO监听之后 - NSKVONotifying_MyPerson MyPerson
person1添加KVO监听之后 - 0x7fff207bf79f 0x1078b6440
类对象 - NSKVONotifying_MyPerson MyPerson
元类对象 - NSKVONotifying_MyPerson MyPerson
可以看出,在添加监听之前和之后methodForSelector
打印出的内存地址发生了变化,可以在控制台里打印出这个内存对应的方法:
(lldb) p (IMP)0x1078b6440
(IMP) $0 = 0x000000000456789c0 (Interview01 ` -[MJPerson setAge:]) at MJPerson.m:13
KVO本质分析
我们发现添加KVO监听之前,person1 和 person2 的类对象是一样的都是 MyPerson,但是添加 KVO 监听之后,person1 的类对象变为了NSKVONotifying_MyPerson。
未使用KVO监听的对象:
使用了 KVO 监听的对象:
_setAge
其实是调用了_NSSetIntValueAndNotify
方法,
_NSSetIntValueAndNotify
方法内部其实是如下伪代码:
// 伪代码
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nol];
}
Foundation
框架内除了有 _NSSetIntValueAndNotify
,还有_NSSetBoolValueAndNotify
、_NSSetCharValueAndNotify
、_NSSetDoubleValueAndNotify
等等类型。
_NSSetxxxValueAndNotify 的内部实现
调用顺序如下:
- 调用 willChangeValueForKey:
- 调用原来的 setter 实现
- 调用 didChangeValueForKey: didChangeValueForKey: 内部会调用 observer 的 observeValueForKeyPath:ofObject:change:context: 方法
NSKVONotifying_MyPerson 除了重写了 setAge: 方法,还重写了 class(屏蔽内部实现,不让开发者知道)、dealloc、_isKVOA 方法。如果调用下面的代码:
NSLog(@"%@ %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"%@ %@", [self.person1 class]), [self.person2 class]);
会发现打印为:
NSKVONotifying_MyPerson MyPerson
MyPerson MyPerson
总结
-
iOS用什么方法实现对一个对象的kvo,kvo的本质是什么?
- 利用Runtime API动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
- willChangeValueForKey:
- 父类原来的Setter
- didChangeValueForKey:
- 内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context)
-
如何手动触发KVO?
- 手动调用willChangeValueForKey:和didChangeValueForKey:
-
直接修改成员变量会触发KVO吗?
- didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context方法。
- OC在运行过程中动态创建了一个类,OC让被监听的对象的isa指向了这个类的全新的子类,然后设置属性值时是调用了这个子类的set方法,这个set方法里调用了一个名为
_NSSetIntValueAndNotify()
的C函数,在这个C函数里又调用了[self willChangeValueForKey:@"age"]
、[super setAge:age]
和[self didChangeValueForKey:@"age"]
,然后在didChangeValueForKey
里又通知了观察者,完整的子类代码实现如下:
@implementation NSKVONotifying_MJPerson
// 在`setAge:`方法中调用了`_NSSetIntValueAndNotify()`这个C语言函数
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify()
}
//伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
// 2.调用父类的`setAge:`函数
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 调用监听器的监听方法,通知监听器某一个对象的属性值发生了改变
[observer observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
- 并不是所有的属性都能监听的
- 反编译工具:Hopper
网友评论