好久没写了, 就拿我没写过的
KVO开刀吧~~有工作的坑位的小伙伴,私信留言喔~~
面试好难哦
1. 什么是KVO?
简介
KVO全称是Key-Value Observing, 通俗讲就是键值监听 , 可以用于监听某个对象属性值的改变,来回调观察者的observeValueForKeyPath: ofObject: change:context:方法。
2. KVO的基本使用
三部曲:
-
注册
KVOobserver: 观察者keyPath:观察属性options:枚举 观察newValue还是oldValuecontext- 参数为空时使用
NULL
- 参数为空时使用
[self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL] -
dealloc移除KVO[self.person1 removeObserver:self forKeyPath:@"age"]; -
书写
KVO回调函数。- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"监听到了%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context); }
3. KVO 存在的疑问
@interface Person : NSObject
@property(assign, nonatomic) int age;
@end
@implementation Person
@end
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 对person1对象添加观察者,观察属性age
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
有如上代码,一样的类, 生成不同的对象,对person1对象age的改变,就可以被走到回调方法。 对peron2对象age的改变就不会。同样的类,同样的方法调用 ,这是为什么?所以这个需要我们去研究他们的不用。
4. KVO的原理分析
经过以上截图调试,发现被添加观察者的person1对象它的isa发生了改变。改变成了NSKVONotifying_Person这个类对象,就不再指向以前的Person类对象。
这个类是通过Runtime API生成的中间类,它继承我们原有的Person类,重写了被观察属性的setter方法。
从以上得出这个图
从本质看,像self.person1.age = 11; self.person2.age = 13;都是调用对象的setter方法。调用person1对象会发送通知,调用person2对象不会发送通知。
因为实例对象的方法存在它的类对象中,发送
setAge方法,首先通过isa去找到它的类对象,查找此方法。在这里我们找到NSKVONotifying_Person类。找到了重写的方法。即调用中间类的setter方法。而
person2对象调用的setter方法。因为它的isa还是Person类对象,所以调用还是Person类里的Setter方法。通过官方文档 看得出,
Automatic key-value observing的实现使用了isa-swizzling技术
⚠️ _NSSetIntValueAndNotify这个方法不是固定的_NSSet【XXXXXX】ValueAndNotify中间括起来的是会根据类型而改变。
比如把int age改成double age,相应的这个就会是 _NSSetDoubleValueAndNotify
4.1 KVO生成的中间类为什么重写class方法?
上面说到,新生成的类除了重写我们的setter方法,还有_isKVOA以及重写的dealloc 和class方法。
像_isKVOA方法返回true标明是KVO,deallco方法作一些清理工作。那class为什么还要重写
通过Runtime底层API获取的类对象是isa真正指向的类对象
和使用OC方法获取的类对象是中间类重写的class方法的返回值
- 从开发角度来看。我们使用
Person类生成实例对象, 我们认知的类型就是Person, 虽然系统中间生成了中间类,它的isa指向改变了,但是苹果并不想把这个中间类直接暴露出来。所以中间类重写了class方法, 直接返回了Person类。 好处就是屏蔽了内部实现,隐藏这个类的实现,在OC代码层面保持一致性。- 如果子类不重写这个方法,调用
class方法,首先通过isa指向去类对象NSKVONotifying_Person查找这个方法,没有找到就一层一层通过superclass查找,直到NSObject类, 默认实现就是直接返回return object_getClass(self), 相当于直接返回NSKVONotifying_Person这个类对象。- 官方文档可以得知,
isa指针是维护调度表的对象类,该表包含指向实现的方法指针,以及其它。当被观察者属性被观察者对象观察时,被观察对象的isa就改变了,因此,isa不一定反映实例对象真实的实际类。告诉我们不要依赖isa指针来确定类型,相反,使用class方法来确定对象实际类型。
// 遍历打印类方法
- (void)printMethodNameOfClass:(Class)cls {
unsigned int count = 0;
NSMutableString *methodNames = [NSMutableString string];
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
[methodNames appendString:NSStringFromSelector(method_getName(method))];
[methodNames appendString:@"\n"];
}
free(methodList);
NSLog(@"%@", methodNames);
}
// 这里也可以这么调用, ⚠️ 必须在注册完KVO后调用个,否则报错, 因为这个类只有在注册KVO后才会动态生成。
// [self printMethodNameOfClass:NSClassFromString(@"NSKVONotifying_Person")];
- (void)printSuperClassChainOfClass:(Class)cls {
// 注册类的总个数
int count = objc_getClassList(NULL, 0);
// 创建一个数组,其中包含给定的对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class *classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i < count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
4.2 调用顺序分析
大概模拟了下中间类的运作NSKVONotifying_Person,子类先去调用Foundation框架的方法,在这个方法里先调用willChangeValueForKey方法,在去调用父类的实现, 最后调用didChangeValueForKey去通知Observer。
4.3 移除观察者后发生什么?
移除观察者后isa指回原来的类,并且动态子类不会销毁,通过打印它还是存在的。
5. Foundation窥探
通过调用顺序分析知道, KVO是在重写的setter方法调用了_NSSetIntValueAndNotify这个方法, 而这个方法属于Foundation框架中, 无法看到其源码实现,那我们从另一种角度来看下Foundation内部的实现
通过image list调试查看Foundation位置,找到它通过Hopper反汇编工具来查看
删除Int后搜索_NSSetValueAndNotify,出来好多各种类型的方法名字, 也就证明了,KVO观察的各种类型属性会根据属性的类型不同调用不同的Foundation方法。
通过终端命令nm加过滤, 搜索查看这些方法名字。
6. 手动关闭&手动发送KVO
当我们在一个类中,想关闭某个属性的KVO或者全部关闭(只有某个属性可以使用KVO)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
// 对某个key禁用
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"nick"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
// 简便写法 直接对nick属性禁止
+ (BOOL)automaticallyNotifiesObserversOfNick {
return YES;
}
-
使用
automaticallyNotifiesObserversForKey返回NO,对象的全部属性都不可以KVO; -
简便写法,是根据对象属性的重写方法
automaticallyNotifiesObserversOf<Key>形式 -
如果全部属性禁止
KVO了, 但又想单独对其中一个属性使用,可以手动触发- (void)setNick:(NSString *)nick{ [self willChangeValueForKey:@"nick"]; _nick = nick; [self didChangeValueForKey:@"nick"]; }- 使用成对的
willChangeValueForKey和didChangeValueForKey手动触发,⚠️如果只调用didChangeValueForKey也是不会触发,因为在didChangeValueForKey会判断willChangeValueForKey;
- 使用成对的
7. 可变数组如何触发KVO
在Person中又一个可变数组
@interface LGPerson : NSObject
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
self.person = dateArray = [NSMutableArray arrayWithCapacity:1];
// 这样会触发KVO吗????
[self.person.dateArray addObject:@1000];
显然通过[self.person.dateArray addObject:@1000];这行代码不会触发KVO,因为KVO本质是要去调用setter方法。 这里显然并没有。
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@22];
LAST - 面试题
1. iOS用什么方式实现对一个对象的KVO? (KVO本质是什么)?
- 利用
RuntimeAPI动态生成子类,并且让实例对象isa指向全新生成的子类 (也就是isa-swizzling技术) - 修改属性会调用 通过重写属性的
setter方法调用Foundation的_NSSetXXXValueNotify函数、、willChangeValueForKey- 父类原来的实现
didChangeValueForKey内部会触发监听器observerValueForKeyPath:ofObject:chang:context方法
- 动态子类重写了观察属性
setter方法class方法,dealloc方法 以及新增isKVOA方法
2. 如何手动处触发KVO?
使用成对的
willChangeValueForKey和didChangeValueForKey手动触发。单独使用didChangeValueForKey也无法触发,必须成对包裹。
3. 直接修改成员变量会处罚KVO吗?
不会,如
self.person->age = 10;根据本质来看,需要调用setter方法
4. 通过kvc调用会触发kvo吗
会触发
KVO, 如[self.person setValue:@"zhangsna" forKey:@"name"]它会触发
setter方法。所以会触发KVO回调。