本文是Objective-C系列的第9篇,主要讲述了KVO的底层实现,以及KVC的使用及KVC中调用流程。html
KVO
全称Key Value Observing
,是苹果提供的一套事件通知机制。容许对象监听另外一个对象特定属性的改变,并在改变时接收到事件。因为KVO
的实现机制,只针对属性才会发生做用,通常继承自NSObject
的对象都默认支持KVO
。git
KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO
是一对一的,而不是一对多的。KVO
对被监听对象无侵入性,不须要修改其内部代码便可实现监听。github
KVO
能够监听单个属性的变化,也能够监听集合对象的变化。经过KVC
的mutableArrayValueForKey:
等方法得到代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。api
项目代码KVO-01-usage数组
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[BFPerson alloc] init];
self.person1.age = 28;
self.person1.name = @"weng";
[self addObserver];
}
- (void)addObserver
{
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
[self.person1 addObserver:self forKeyPath:@"name" options:option context:@"name change"];
}
复制代码
/** 观察者监听的回调方法 @param keyPath 监听的keyPath @param object 监听的对象 @param change 更改的字段内容 @param context 注册时传入的地址值 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
复制代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 29;
self.person1.name = @"hengcong";
}
复制代码
//下面调用方式均可以出发KVO
self.person1.age = 29;
[self.person1 setAge:29];
[self.person1 setValue:@(29) forKey:@"age"];
[self.person1 setValue:@(29) forKeyPath:@"age"];
复制代码
KVO在属性发生改变时的调用是自动的,若是想要手动控制这个调用时机,或想本身实现KVO属性的调用,则能够经过KVO提供的方法进行调用。bash
下面以age
属性为例:app
//age不须要自动调用,age属性以外的(含name)自动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"age"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
复制代码
上面方法也等同于下面两个方法:ide
+ (BOOL)automaticallyNotifiesObserversOfAge
{
return NO;
}
复制代码
针对每一个属性,KVO都会生成一个**‘+ (BOOL)automaticallyNotifiesObserversOfXXX’**方法,返回是否能够自动调用KVO函数
假如实现上述方法,咱们会发现,此时改变age
属性的值,没法触发KVO,还须要实现手动调用才能触发KVO。工具
- (void)setAge:(NSInteger)age
{
if (_age != age) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
}
复制代码
实现了(1)禁用自动调用(2)手动调用实现 两步,age
属性手动调用就实现了,此时能和自动调用同样,触发KVO。
- (void)dealloc
{
[self removeObserver];
}
- (void)removeObserver
{
[self.person1 removeObserver:self forKeyPath:@"age"];
[self.person1 removeObserver:self forKeyPath:@"name"];
}
复制代码
KVO若使用不当,极容易引起Crash。相关试验代码在KVO-02-crash
若观察者对象**-observeValueForKeyPath:ofObject:change:context:**未实现,将会Crash
Crash:Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<ViewController: 0x7f9943d06710>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Crash: Thread 1: EXC_BAD_ACCESS (code=1, address=0x105e0fee02c0)
//观察者ObserverPersonChage
@interface ObserverPersonChage : NSObject
//实现observeValueForKeyPath: ofObject: change: context:
@end
//ViewController
- (void)addObserver
{
self.observerPersonChange = [[ObserverPersonChage alloc] init];
[self.person1 addObserver:self.observerPersonChange forKeyPath:@"age" options:option context:@"age chage"];
[self.person1 addObserver:self.observerPersonChange forKeyPath:@"name" options:option context:@"name change"];
}
//点击按钮将观察者置为nil,即销毁
- (IBAction)clearObserverPersonChange:(id)sender {
self.observerPersonChange = nil;
}
//点击改变person1属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 29;
self.person1.name = @"hengcong";
}
复制代码
假如在当前ViewController中,注册了观察者,点击屏幕,改变被观察对象person1的属性值。
点击对应按钮,销毁观察者,此时self.observerPersonChange为nil。
再次点击屏幕,此时Crash;
Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path "age" from <BFPerson 0x6000014acd00> because it is not registered as an observer.
在注册Observe时,传入keyPath为字符串类型,keyPath极容易误写。
[self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
复制代码
优化的方案是:
[self.person1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:option context:@"age change"];
复制代码
/** 若是age改变 观察者也会收到name改变的通知 */
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAge
{
NSSet *set = [NSSet setWithObjects:@"name", nil];
return set;
}
复制代码
为了区分在添加KVO以后,对象以及对应的属性设值方法发生的变化,咱们进行了以下测试:
观察方法实现:
person1
指向的类对象
和元类对象
,以及setAge:
均发生了变化;person1
中的isa
指向了NSKVONotifying_BFPerson类对象;setAge:
的实现调用的是:Foundation 中 _NSSetLongLongValueAndNotify
方法;重写项目在KVO-03-princlipe。
咱们经过重写方法后,进行打印测试
结合
BFPerson willChangeValueForKey: - begin
BFPerson willChangeValueForKey: - end
BFPerson setAge: begin
BFPerson setAge: end
BFPerson didChangeValueForKey: - begin
-[ViewController observeValueForKeyPath:ofObject:change:context:]---监听到BFPerson的age属性值改变了 - {
kind = 1;
new = 29;
old = 28;
} - age chage
BFPerson didChangeValueForKey: - end
咱们整理出一个完整的方法链:
以下图:
可是这些不足以反应真正完整的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 theclass
method to determine the class of an object instance.
咱们同时观察添加KVO以后,中间对象的方法列表以及未添加以前的方法列表:
类 | 方法列表(忽略name属性相关方法) |
---|---|
BFPerson | test, .cxx_destruct, setAge:, age |
NSKVONotify_BFPerson | setAge:, class, dealloc, _isKVOA |
isa
交换技术
交换以后,调用任何BFPerson
对象的方法,都会通过NSKVONotify_BFPerson
,可是不一样的方法,有不一样的处理方式。
setAge:
,都会先调用NSKVONotify_BFPerson
对应的属性设置方法;test
,会经过NSKVONotify_BFPerson
的superclass
,找到BFPerson
类对象,再调用其[BFPerson test]
方法交换以后,isa
指向的并非该类的真实反映,一样object_getClass
返回的是isa
指向的对象,因此也是不可靠的。
好比使用KVO以后,经过object_getClass
获得的是生成的中间对象NSKVONotify_BFPerson
,而不是BFPerson
。
要想得到该类真实的对象,须要经过class
对象方法获取。
假如经过**[self.person1 class]**获得的是BFPerson
对象。
**[self.person1 class]**获得的仍然是BFPerson
对象,为何?
NSKVONotify_BFPerson
重写了其class
对象方法,返回的是BFPerson
;_isKVOA
返回是不是KVO;
delloc
作一些清理工做
到此,基本上NSKVONotifying_BFPerson
类已经成型(相关代码参考项目),结合调用流程,咱们绘制出下面对比图。
项目源码在:KVC-01-usage
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
复制代码
其中,有两个方法要注意:
valueForKey | objectForKey | |
---|---|---|
无key的处理 | 无该key,crash,NSUndefinedKeyException | 无该key返回nil |
来源 | KVC主要方法 | NSDictionary的方法 |
符号 | 若以 @ 开头,去掉 @ ,用剩下部分做为 key 执行 [super valueForKey:] | key 不是以 @ 符号开头, 二者等同 |
setValue | setObject | |
---|---|---|
value | value可为nil,当value为nil的时候,会自动调用removeObject:方法 | value是不能为nil |
来源 | KVC的主要方法 | NSMutabledictionary特有的 |
key的参数 | 只能是NSString | setObject: 可任何类型 |
NSKeyValueCoding类别中还有其余的一些方法,例如
//默认返回YES,表示若是没有找到set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值确认的API,它能够用来检查set的值是否正确、为不正确的值作一个替换值或者拒绝设置新值并返回错误缘由。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操做的API,里面还有一系列这样的API,若是属性是一个NSMutableArray,那么能够用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//若是Key不存在,且没有KVC没法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法同样,只不过是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//若是你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
复制代码
有序集合对应方法以下:
-countOf<Key>//必须实现,对应于NSArray的基本方法count:2
-objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后能够提升性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,相似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,相似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://可选的,若是在此类操做上有性能问题,就须要考虑实现之
复制代码
无序集合对应方法以下:
-countOf<Key>//必须实现,对应于NSArray的基本方法count:
-objectIn<Key>AtIndex:
-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:
-get<Key>:range://不是必须实现的,但实现后能够提升性能,其对应于 NSArray 方法 getObjects:range:
-insertObject:in<Key>AtIndex:
-insert<Key>:atIndexes://两个必须实现一个,相似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:
-removeObjectFrom<Key>AtIndex:
-remove<Key>AtIndexes://两个必须实现一个,相似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:
-replaceObjectIn<Key>AtIndex:withObject:
-replace<Key>AtIndexes:with<Key>://这两个都是可选的,若是在此类操做上有性能问题,就须要考虑实现之
复制代码
例如设置:UITextField中的placeHolderText
[textField setValue:[UIFont systemFontOfSize:25.0] forKeyPath:@"_placeholderLabel.font"];
复制代码
如何获取控件的内部属性?
unsigned int count = 0;
objc_property_t *properties = class_copyPropertyList([UITextField class], &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
const char *name = property_getName(property);
NSLog(@"name:%s",name);
}
复制代码
当对容器类使用KVC时,valueForKey:将会被传递给容器中的每个对象,而不是容器自己进行操做。结果会被添加进返回的容器中,这样,开发者能够很方便的操做集合来返回另外一个集合。
NSArray *arr = @[@"ali",@"bob",@"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str in arrCap) {
NSLog(@"%@",str); //Ali\Bob\Cydia
}
复制代码
简单集合运算符
@interface Book : NSObject
@property (nonatomic,assign) CGFloat price;
@end
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
复制代码
对象运算符
// 获取全部Book的price组成的数组,而且去重
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
复制代码
Array和Set操做符(集合中包含集合的情形)
项目源码在**KVC-02-principle**
setValue:forKey
valueForKey
//抽出Foundation库,查看其中Notify的函数
$ nm ./Foundation | grep LongValueAndNotify
22bc9290 t __NSSetLongLongValueAndNotify
22bc90a0 t __NSSetLongValueAndNotify
22bc93a0 t __NSSetUnsignedLongLongValueAndNotify
22bc9198 t __NSSetUnsignedLongValueAndNotify
22bc9d18 t ____NSSetLongLongValueAndNotify_block_invoke
22bc9ca8 t ____NSSetLongValueAndNotify_block_invoke
22bc9d4c t ____NSSetUnsignedLongLongValueAndNotify_block_invoke
22bc9ce0 t ____NSSetUnsignedLongValueAndNotify_block_invoke
复制代码
KVOController Facebook出品的KVO封装库。