iOS面试题库——KVC与KVO
KVC与KVO
本文将会详解在面试中的常客——KVO实现的原理,在了解KVO之前我们要对KVC进行一个全面的了解,毕竟连官方文档都提到过:
important: In order to understand key-value observing, you must first understand key-value coding.
1.1KVC
KVC全称:Key-value coding(键-值编码),通过KVC机制我们可以间接的访问对象的属性。而KVC之所以能够访问属性是因为对象遵守了一个非正式的NSKeyValueCoding
协议(NSObject开始就遵守了此协议,所以继承自NSObject的对象都可以使用KVC)。开发中我们都知道在对于属性可以使用getter
和setter
或者直接使用实例变量来进行直接访问和修改。但这些访问方式是需要依靠属性的get
方法、set
方法、变量名。随着对象定义的属性增加或者变动。编译器生成的这些getter
、setter
会越来越多。KVC则是通过是用字符串的名字Key来对属性进行访问和修改。
KVC中最关键的两个方法:
-valueForKey:
-setValue:forKey:
1.1.1 valueForKey:
-valueForKey:
是通过\来获取属性的值。在一个对象实例中按get<Key>
、<key>
、is<Key>
、_<key>
顺序匹配。命中的Value的类型如果是对象直接返回。如果命中的Value是能被包装成NSNumber的数值类型。包装成NSNumber返回。不支持NSNumber的数值类型则包装成NSValue返回。如果没用命中调用-valueForUndefinedKey:
抛出异常,valueForUndefinedKey:
可在子类中重写忽略抛出的异常,自己处理。
@interface Person
@property (nonatomic, assign) CGFloat height;
@end
p.height = 119.0;
NSNumber *height = [p valueForKey:@"height"]; //CGFloat 包装成NSNumber。
注意
- 上述的查找过程中省略了很多其他情况下的查找
countOf<Key>
,objectIn<Key>AtIndex:
,countOf<Key>
,enumeratorOf<Key>
,memberOf<Key>:
,有兴趣的同学可以去Search Pattern for the Basic Getter查看详细的查询路径。- 非Object对象包装成NSNumber或NSValue列表查看Representing Non-Object Values
1.1.2 setValue:forKey:
-setValue:forKey:
同-valueForKey:
也是根据给定的\匹配方法名set<Key>:
、_set<Key>
,如果命中,调用方法将Value作为参数传入。未命中则看+ accessInstanceVariablesDirectly
是否返回YES,YES则按 _<key>
, _is<Key>
, <key>
, is<Key>
顺序匹配实例变量。如果命中直接将Value给变量赋值。NO则调用-setValue:forUndefinedKey:
抛出异常。-setValue:forUndefinedKey:
也可以被子类重写。
// setValue: forKey:使用
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
1.2 KVO
1.2.1 使用
KVO
在iOS中是观察者模式的一种表现。我们可以使用KVO
让某个对象成为另外一个对象的监听者。当被监听对象的属性发生改变时,KVO
就会通知监听者。
关于KVO
的使用网上有很多教程,KVO使用主要是三个步骤:
- 调用
addObserver:forKeyPath:options:context:
注册成为监听者。 - 监听者实现
observeValueForKey:ofObject:change:context:
方法。 - 调用
removeObserver:forKeyPath:
移除监听
前面说过KVO
的实现是建立在KVC
的基础上的。即被监听的属性必须能满足KVC
的,才能是用KVO
来监听。
KVO的自动触发监听通知的方法系列:
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
KVO的手动触发监听通知:
// 关闭balance的自动触发
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
要实现手动触发监听,你要执行在变更前执行willChangeValueForKey:
方法,在变更后执行didChangeValueForKey:
方法:
- (void)setBalance:(double)balance {
[self willChangeValueForKey:@"balance"];
_balance = balance;
[self didChangeValueForKey:@"balance"];
}
1.2.2 原理
>
Automatic key-value observing is implemented using a technique called isa-swizzling.
上面这句话在Key-Value Observing Implementation Details提到。意思就是KVO的是通过一种叫isa-swizzling
的技术实现的。
查看NSObject.h文件。
@interface NSObject <NSObject> {
Class isa;
}
control + command
点击Class
,看到Class
实际上是一个指向obj_class结构体的指针。
typedef struct objc_class *Class;
struct objc_class {
struct objc_class * isa; // 原始的代码 Class isa;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到在objc_class
结构体中还有一个isa,即类中还有一个指向objc_class
的指针。类的isa指向的是元类(metaClass
)的。如果将对象看成是通过类的实例。那么类就是元类的实例咯?
。
这篇文章中我们仅了解什么是Class isa
。objc_class
结构体中的其它部分会单开一篇runtime
的文章来写。
typedef struct objc_object *id;
struct objc_object {
struct objc_class * isa; // 原始的代码 Class isa;
};
id
在obj
中可以代表个对象。id又是一个objc_object
结构体的指针,且objc_object
结构体中的isa
指向的是该对象的类。
在这里是时候祭出火遍runtime
界的图: .
回到KVO
。前文提到的isa-swizzling
对于了解过runtime
的同学可能对此有点眼熟。method swizzling也是runtime
的一种黑魔法。可以通过method swizzling
进行方法的互换。回到KVO
上面,我们猜测isa-swizzling
就是类似method swizzling
,只不过是对Class的交换。
在KVO中当一个监听者被注册被监听的对象上时。被监听对象的isa
指针已经被更改了。被监听对象的isa
指针被修改为指向为一个中间类。改中间类可能是该类的子类。重写了被监听对象的属性。然后在改属性值被修改时,会触发监听通知。
KVO
的详细流程
- 监听者调用监听的方法。
- 被监听者派生一个中间类。被监听对象的
isa
指针指向派生类 - 被监听的属性发生变化,由中间类触发监听通知(具体方式未知)。
- 监听者收到通知。触发
observeValueForKey:ofObject:change:context: