面试题
问题一:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
问题二:如何手动触发KVO?(就算没有人修改age值,也想触发监听方法observeValueForKeyPath)
问题三:直接修改成员变量会触发KVO吗?
KVO的基本使用
KVO我们经常使用,KVO的全称是: Key-Value Observing ,俗称"键值监听",可以用来监听某个对象的属性改变.
为了接下来的研究,我们创建一个MJPerson
类,类中添加一个age
属性,然后在ViewController
中创建person1
,person2
两个属性,给person1
添加观察者,如下:
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person1.height = 11;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
self.person2.height = 22;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 addObserver:self forKeyPath:@"height" options:options context:@"456"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 20;
self.person2.age = 20;
self.person1.height = 30;
self.person2.height = 30;
}
//移除监听:
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"height"];
}
// 当监听对象的属性值发生改变时,就会调用
/**
当监听对象的属性值发生改变时,就会调用
@param keyPath 监听的KeyPath
@param object 被监听的对象
@param change 改变
@param context 监听时传入的context
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
@end
RUN>
================打印结果================ 2021-04-16 14:54:13.669370+0800 Interview01[5093:145779] 监听到<MJPerson: 0x60000058c5a0>的age属性值改变了 - { kind = 1; new = 20; old = 1; } - 123 2021-04-16 14:54:13.669516+0800 Interview01[5093:145779] 监听到<MJPerson: 0x60000058c5a0>的height属性值改变了 - { kind = 1; new = 30; old = 11; }
KVO底层是怎么实现的
为了探究KVO的底层是怎么实现的,我们创建person1和person2,其中person1添加监听,person2不添加监听,代码如下:
#import "ViewController.h"
#import "MJPerson.h"
@interface ViewController ()
@property (strong, nonatomic) MJPerson *person1;
@property (strong, nonatomic) MJPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// self.person1.age = 21;
// self.person2.age = 22;
// NSKVONotifying_MJPerson是使用Runtime动态创建的一个类,是MJPerson的子类
//如果你自己写了这个类,就会报动态生成失败
//KVO效率没代理高,因为代理是直接调用,KVO还要动态生成一个类
// self.person1.isa == NSKVONotifying_MJPerson
[self.person1 setAge:21];
// self.person2.isa = MJPerson
[self.person2 setAge:22];
}
- (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);
}
@end
RUN>
================打印结果================ 2021-04-16 15:55:57.767175+0800 Interview01[5155:149188] 监听到<MJPerson: 0x600002dc42d0>的age属性值改变了 - { kind = 1; new = 21; old = 21; } - 123
发现监听到了
person1
属性值得变化,而person2
并没有变化,为什么会这样呢?我们看看他们给age
赋值的方法:[self.person1 setAge:21]
;[self.person2 setAge:22];
#import "MJPerson.h" @implementation MJPerson - (void)setAge:(int)age { _age = age; } //- (int)age //{ // return _age; //} @end
person1
和person2
的setter
方法都是一样的,为什么结果会有这么大的差别呢?首先问题肯定不会在
setter
方法上,因为两个setter
都是一样的,问题就是出在person1
和person2
两个对象上,我们打印person1
和person2
的isa
看看:可以发现,person1添加监听后isa是
NSKVONotifying_MJPerson
,person2不添加监听isa还是MJPerson
。NSKVONotifying_MJPerson是系统利用Runtime动态创建的一个类,是MJPerson的子类。
既然NSKVONotifying_MJPerson也是一个类,那么它肯定也有自己的isa和superclass,未使用KVO和使用KVO,实例对象和类对象内存结构如下:
当person2不添加监听的时候,值改变,会通过person2的isa找到MJPerson,然后再找到MJPerson里面的setAge方法调用,完成。
当person1添加监听的时候,值改变,会通过person1的isa找到NSKVONotifying_MJPerson,然后调用NSKVONotifying_MJPerson的setAge方法(方法内部会调用Foundation框架的_NSSetIntValueAndNotify),不会直接调用MJPerson的setAge方法了。
NSKVONotifying_MJPerson
重写父类的setAge:
方法内部调用了Foundation
框架的_NSSetIntValueAndNotify
方法.通过方法名我们可以大概猜测出在这个方法内部先设置了属性的值,然后再通知外部.它的伪代码大概如下:#import "MJPerson.h" @interface NSKVONotifying_MJPerson : MJPerson @end #import "NSKVONotifying_MJPerson.h" @implementation NSKVONotifying_MJPerson - (void)setAge:(int)age { _NSSetIntValueAndNotify(); } // 伪代码 void _NSSetIntValueAndNotify() { [self willChangeValueForKey:@"age"]; [super setAge:age]; [self didChangeValueForKey:@"age"]; } - (void)didChangeValueForKey:(NSString *)key { // 通知监听器,某某属性值发生了改变 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil]; } @end
现在我们终于明白了KVO的大概原理,我们在给一个对象添加KVO时,runtime会动态的生成一个相关联的派生类,然后重写了
setter
方法,在setter
方法内部进行了一些的操作,达到监听的目的.总结:
- 使用KVO,系统会使用Runtime动态创建的一个NSKVONotifying_MJPerson类,这个类是MJPerson的子类。
- 添加监听的属性的值改变的时候,会调用NSKVONotifying_MJPerson类的setAge方法,setAge方法里面会调用
_NSSetIntValueAndNotify
方法,_NSSetIntValueAndNotify里面走如下步骤:
① willChangeValueForKey 将要改变
② setAge(原来的set方法) 真的去改变
③ didChangeValueForKey 已经改变
④ observeValueForKeyPath:ofObject:change:context: 监听到MJPerson的age属性改变了
验证_NSSetIntValueAndNotify
首选我们在person1
添加KVO之前和之后分别打印person1
和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监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
RUN>
================打印结果================ 2021-04-16 16:12:30.888688+0800 Interview01[5696:209723] person1添加KVO监听之前 - 0x104c604b0 0x104c604b0 2021-04-16 16:12:30.888920+0800 Interview01[5696:209723] person1添加KVO监听之后 - 0x7fff207bc2b7 0x104c604b0
可以发现,添加KVO之前
setAge:
方法实现都相同,添加KVO之后,person1
的setAge:
方法实现发生改变了. 那我们怎么知道的setAge:
方法中调用了_NSSetIntValueAndNotify
方法的呢?我们在LLDB中打印这两个地址的实现
(lldb) p (IMP) 0x7fff207bc2b7 (IMP) $0 = 0x00007fff207bc2b7 (Foundation`_NSSetIntValueAndNotify) (lldb) p (IMP) 0x104c604b0 (IMP) $1 = 0x0000000104c604b0 (Interview01`-[MJPerson setAge:] at MJPerson.m:13) (lldb)
person1
添加KVO后,他的setAge:
方法实现就是_NSSetIntValueAndNotify
.
添加KVO后的person1
的类对象和元类对象和person2
一样吗?
NSLog(@"person1添加KVO监听之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2));
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
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
RUN>
================打印结果================ 2021-04-16 16:22:35.516106+0800 Interview01[5795:223652] person1添加KVO监听之前 - MJPerson MJPerson 2021-04-16 16:22:35.516327+0800 Interview01[5795:223652] 类对象 - NSKVONotifying_MJPerson MJPerson 2021-04-16 16:22:35.516430+0800 Interview01[5795:223652] 元类对象 - NSKVONotifying_MJPerson MJPerson
通过打印结果可以看出,添加KVO后类对象和元类对象都是runtime动态生成的,跟之前的并不一样.,添加KVO之后,
person1
的类对象和元类对象都是NSKVONotifying_MJPerson
类型
验证_NSSetIntValueAndNotify内部方法调用流程
伪代码演示了_NSSetIntValueAndNotify
方法内部大概如下:
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
验证过程也很简单,重写MJPerson类的三个方法,如下:
#import "MJPerson.h"
@implementation MJPerson
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
//- (int)age
//{
// return _age;
//}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
RUN>
================打印结果================ 2021-04-16 16:30:36.386996+0800 Interview01[5836:233476] willChangeValueForKey 2021-04-16 16:30:36.387173+0800 Interview01[5836:233476] setAge: 2021-04-16 16:30:36.387257+0800 Interview01[5836:233476] didChangeValueForKey - begin 2021-04-16 16:30:36.387593+0800 Interview01[5836:233476] 监听到<MJPerson: 0x6000008405b0>的age属性值改变了 - { kind = 1; new = 21; old = 1; } - 123 2021-04-16 16:30:36.387677+0800 Interview01[5836:233476] didChangeValueForKey - end
从打印结果可以看出来的确是先调用
willChangeValueForKey
,然后又调用了[super setAge:age]
,先进入didChangeValueForKey
方法,然后再发出通知
验证重写class、dealloc、isKVO方法
面说过NSKVONotifying_MJPerson
类中有两个成员变量isa
和superClass
和4个方法setAge:
,class
,dealloc
,_isKVO
.下面我们写一个方法证明一下NSKVONotifying_MJPerson
的确存在这几个方法.setAge方法我们知道为什么重写,但是为什么要重写后面三个方法呢?
首先我们先验证NSKVONotifying_MJPerson的确有这四个方法: 写一个方法,打印输出一个类所有的方法:
//获取一个类里面所有的方法
- (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:@", "];
}
//c语言中,如果数组是create或者copy出来的要free OC中ARC不用管
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
RUN>
================打印结果================ 2021-04-16 16:39:39.741528+0800 Interview01[5920:244465] NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA, 2021-04-16 16:39:39.741651+0800 Interview01[5920:244465] MJPerson setAge:, age,
由打印结果可知:
NSKVONotifying_MJPerson里面的确有setAge:、class、dealloc、_isKVOA四个方法
MJPerson里面有setAge:、age两个方法为什么
NSKVONtifying_HHPerson
类中为什么会重写class
方法?如果不重写会怎么样?我们知道打印一个类的类对象有好几种方法:
object_getClass
和class
.我们分别用这两种方法打印对比一下:NSLog(@"person1: %@, person2:%@",object_getClass(self.person1),object_getClass(self.person2)); NSLog(@"person1: %@, person2:%@",[self.person1 class],[self.person2 class]);
RUN>
================打印结果================ 2021-04-16 16:43:29.670250+0800 Interview01[5948:249055] person1: NSKVONotifying_MJPerson, person2:MJPerson 2021-04-16 16:43:29.670334+0800 Interview01[5948:249055] person1: MJPerson, person2:MJPerson
从打印结果可以看到,
object_getClass
打印出了真实类型,而class
打印的结果都是一样,这是因为苹果官方并不想暴露NSKVONotifying_MJPerson
这个类,它想隐藏KVO实现的细节,如果没有重写class
这个方法,那么最终会找到NSObject
中的class
方法,执行object_getClass(self)
打印的还是NSKVONotifying_HHPerson
.所以苹果官方为了隐藏KVO的实现细节,重写了class
方法.其实,因为NSKVONotifying_MJPerson是内部创建的,不想让用户看到,所以用户调用class方法要把NSKVONotifying_MJPerson转成MJPerson,所以系统才重写了class方法。使用object_getClass函数(RuntimeAPI)获取的就是真实的,不会被转成MJPerson。
如果NSKVONotifying_MJPerson没有实现class方法,最后会调用到NSObject的class方法,会直接返回NSKVONotifying_MJPerson,因为NSObject内部这样实现的:
@implementation NSObject - (Class)class { return object_getClass(self); } @end
我们可以写NSKVONotifying_MJPerson的伪代码:
#import "NSKVONotifying_MJPerson.h" @implementation NSKVONotifying_MJPerson //NSKVONotifying_MJPerson内部实现了setKey class dealloc isKVO 方法 - (void)setAge:(int)age { _NSSetIntValueAndNotify(); } // 屏蔽内部实现,隐藏了NSKVONotifying_MJPerson类的存在 - (Class)class { return [MJPerson class]; } - (void)dealloc { // 收尾工作 } - (BOOL)_isKVOA { return YES; } @end
面试题
下面我们就可以回答面试题了:
问题一:iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答:
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会先调用这个新子类的setter方法,这个新子类的setter方法内部会调用Foundation的_NSSet*ValueAndNotify函数(内部调用如下方法)
- willChangeValueForKey:
- 父类原来的setter
- didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
问题二:如何手动触发KVO?(就算没有人修改age值,也想触发监听方法observeValueForKeyPath)
答:手动调用willChangeValueForKey:和didChangeValueForKey:
比如:
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 2;
[self.person1 didChangeValueForKey:@"age"];
问题三:直接修改成员变量会触发KVO吗?
答:不会触发KVO,因为没调用重写后的set方法。
比如,如下代码,不会触发
self.person1->_age = 2;
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!