面试题目
iOS
用什么方式实现对一个对象的KVO
?(KVO
的本质是什么?)- 如何手动触发
KVO
?
上面两道面试题目,都是在考察程序员对KVO
的理解。KVO
对于一个iOS
程序员来讲并不陌生,在实际开发中我们或多或少都会使用到,在某些场景中,会让我们的开发变得更加简单。那么你了解KVO
的本质吗?它是如何实现的呢?
KVO
简介
在这里帮大家回忆一下KVO
,对于熟悉的同学向下翻阅。
概述
NSKeyValueObserving
或者 KVO
,是一个非正式协议,它定义了对象之间观察和通知状态改变的通用机制的。KVO
的中心思想其实是相当引人注意的。任意一个对象都可以订阅以便被通知到其他对象状态的改变。这个过程大部分是内建的,自动的,透明的。
KVO
字面翻译就是键值对监听
,允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。一般继承自NSObject
的对象都默认支持KVO
。
KVO
可以监听单个属性的变化,也可以监听集合对象(NSArray
和NSSet
)的变化。
基础使用
- 添加观察者:通过
addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件。 - 回调:在观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者。 - 当观察者不需要监听时,必须调用
removeObserver:forKeyPath:
方法将KVO
移除。
注册观察者
/*
observer:注册KVO通知的对象。观察者必须实现 key-value observing 方法
observeValueForKeyPath:ofObject:change:context:。
keyPath:观察者的属性的 keypath,相对于接受者,值不能是 nil。
options: NSKeyValueObservingOptions 的组合,它指定了观察通知中包含了什
么,可以查看 "NSKeyValueObservingOptions"。
context:在 observeValueForKeyPath:ofObject:change:context: 传给observer
参数的随机数据
*/
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
options
代表NSKeyValueObservingOptions
的位掩码。
NSKeyValueObservingOptionNew = 0x01, 新值
NSKeyValueObservingOptionOld = 0x02, 旧值
NSKeyValueObservingOptionInitial = 0x04, 在注册观察者后,立即接收一次回调
NSKeyValueObservingOptionPrior = 0x08 会在变化前后收到两次回调
context
它可以被用作区分那些绑定同一个keypath
的不同对象的观察者。如何设置一个好的context
?
static void *XXContext = &XXContext;
一个静态变量存着它自己的指针。在CocoaAsynSocket
中也有类似的使用。
当然context
还可以传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO
中的一种传值方式。
回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
这些参数跟我们指定的 –addObserver:forKeyPath:options:context:
是一样的,而change
取决于哪个NSKeyValueObservingOptions
选项被使用。
更好的KeyPath
传字符串做为keypath
比直接使用属性更糟糕,因为任何错字或者拼写错误都不会被编译器察觉,最终导致不能正常工作。 一个聪明的解决方案是使用 NSStringFromSelector
和一个@selector
字面值。
NSStringFromSelector(@selector(isFinished))
因为@selector
检查目标中的所有可用selector
,这并不能阻止所有的错误,但它可用捕获大部分-包括捕获Xcode
自动重构带来的改变。
那么在回调中可能是这样子写的:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([object isKindOfClass:[NSOperation class]]) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {
}
} else if (...) {
// ...
}
}
取消注册
当一个观察者完成了监听一个对象的改变,需要调用removeObserver:forKeyPath:context:
。它经常在observeValueForKeyPath:ofObject:change:context:
,或者dealloc
中被调用。
注意
- 在调用
addObserver
方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期。 -
KVO
的addObserver
和removeObserver
必须成对出现,如果重复removeObserver
则会导致Crash
,如果在观察者释放前,忘记removeObserver
,则会在再次接收到KVO
回调时Crash
。 - 观察者必须实现
key-value observing
方法
observeValueForKeyPath:ofObject:change:context:
,否则会Crash
。 -
KVO
也有对应集合的实现,包括我们常用的NSArray
,可以去KVO
的头文件中查看,有对应的键值参数等。
苹果官方推荐的方式是,在init
的时候进行addObserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,是一种比较理想的使用方式。
KVO
本质
我们可以翻看苹果文档:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
KVO
是通过基于runtime
技术实现的。当某个对象被观察时,系统的runtime运行时会动态的实现一个基于该类的一个中间类(派生类),在中间类中实现被观察基类属性的setter方法,中间类在重写的setter方法中实现真正的通知机制。
同时派生类还重写了- (Class)class;
方法以“欺骗”外部调用者它就是起初的那个类。然后系统将被观察对象的isa
指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对被观察属性setter
的调用就会调用重写的 setter
,从而激活键值通知机制。此外,派生类还重写了dealloc
方法来释放资源。
通过代码验证一波:
/* 被观察对象模型 */
@interface KVOObject : NSObject
@property (nonatomic, copy) NSString *objName;
@end
/* 在控制器的viewDidLoad中 */
- (void)viewDidLoad {
[super viewDidLoad];
self.object_1 = [[KVOObject alloc] init];
self.object_1.objName = @"object_1---前";
[self isaPointerOfObject:self.object_1];
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld;
[self.object_1 addObserver:self forKeyPath:@"objName" options:options
context:kObserverContext];
self.object_1.objName = @"object_1---后";
[self isaPointerOfObject:self.object_1];
}
/* 打印被观察对象所属类... */
- (void)isaPointerOfObject:(KVOObject *)obj{
Class objectMethodClass = [obj class];
Class objectRuntimeClass = object_getClass(obj);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"%@-objectMethodClass:%@",obj.objName,objectMethodClass);
NSLog(@"%@-objectRuntimeClass:%@",obj.objName,objectRuntimeClass);
NSLog(@"%@-superClass:%@",obj.objName,superClass);
}
通过控制台打印的内容对比,对象被KVO
后,其真正类型变为了NSKVONotifying_KVOObject
类,已经不是之前的类,其实是将被观察对象的isa从指向的KVOObject的class,改为系统“偷偷”帮我们创建的NSKVONotifying_KVOObject
类的class。新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。
同时我们发现- (Class)class;
在注册观察者前后,结果却是一致的。这其实是苹果对我们的一种“欺骗”,在中间类中重写了- (Class)class
方法。
/* 打印查看方法*/
- (void)methodListOfObject:(KVOObject *)obj
{
Class objectRuntimeClass = object_getClass(obj);
unsigned int count;
Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"%@-method Name = %@\n",obj.objName,methodName);
}
free(methodList);
}
在新创建的中间类中,重写了setter:
,dealloc
,class
方法,添加新的方法_isKVOA
。dealloc
推测在添加观察者时,增加了一些依赖,需要在对象释放时销毁。_isKVOA
方法,这个方法可以当做使用了KVO
的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO
动态生成的类,就可以从方法列表中搜索这个方法。
那么系统重写了setter:
后是如何实现通知的呢?由于苹果API这部分没有开源,但是通过函数调用栈,我们推测一二。
/* 在KVOObject的.m文件中...*/
@implementation KVOObject
// 是否允许通知..
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
NSLog(@"automaticallyNotifiesObserversForKey:");
return YES;
}
// 重写setter
- (void)setObjName:(NSString *)objName
{
NSLog(@"setter方法,属性发生修改 - 前");
_objName = objName;
NSLog(@"setter方法,属性发生修改 - 后");
}
// 重写willChangeValueForKey
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
// 重写didChangeValueForKey
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - 前");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - 后");
}
@end
控制台打印结果:
函数调用栈:
控制台打印结果和函数调用栈推测:重写的setter:
内部调用了_NSSetObjectValueAndNotify
的方法(这是一个系列的方法,还有_NSSetIntValueAndNotify
、_NSSetDoubleValueAndNotify
等)。
在这个明显C
风格命名的方法内部,
- 先调用了
willChangeValueForKey:
- 然后调用父类的
-setter:
方法,对值进行修改 - 修改完后再调用
didChangeValueForKey:
方法 - 在
didChangeValueForKey:
的内部最终调用了NSKeyValueNotifyObserver
方法,通知属性的观察者,观察者收到了值修改的信息 -
didChangeValueForKey:
调用完毕
至此,在能力范围内的对KVO
内部的分析已经完毕。相信看到这里的同学对 iOS
用什么方式实现对一个对象的KVO
?(KVO
的本质是什么?)已经可以回答出来了。
那如何手动触发KVO
?怎么弄...
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
控制是否自动发送通知,如果返回NO
,KVO
无法自动运作,需手动触发。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"objName"]) {
return NO;
}
return YES;
}
- (void)setObjName:(NSString *)objName
{
if (<#条件判断##>){
[self willChangeValueForKey:@"objName"];
}
_objName = objName;
if (<#条件判断##>) {
[self didChangeValueForKey:@"objName"];
}
}
比如我们想对objName
属性手动出发KVO
,就在+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
中对objName
返回NO
,并在属性-setter
方法中,手动调用willChangeValueForKey
和didChangeValueForKey
(必须两者都调用),这样就可以实现手动来出发KVO
了。