Objective-C(九)KVC与KVO

本文是Objective-C系列的第9篇,主要讲述了KVO的底层实现,以及KVC的使用及KVC中调用流程。html

1、概述

KVO全称Key Value Observing,是苹果提供的一套事件通知机制。容许对象监听另外一个对象特定属性的改变,并在改变时接收到事件。因为KVO的实现机制,只针对属性才会发生做用,通常继承自NSObject的对象都默认支持KVOgit

KVONSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不是一对多的。KVO对被监听对象无侵入性,不须要修改其内部代码便可实现监听。github

KVO能够监听单个属性的变化,也能够监听集合对象的变化。经过KVCmutableArrayValueForKey:等方法得到代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSetapi

2、KVO基本使用

项目代码KVO-01-usage数组

2.1 注册观察者

- (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"];
}
复制代码

2.2 监听回调

/** 观察者监听的回调方法 @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);
}
复制代码

2.3 调用

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person1.age = 29;
    self.person1.name = @"hengcong";
}
复制代码

2.3.1 其余调用方式

//下面调用方式均可以出发KVO
    self.person1.age = 29;
    [self.person1 setAge:29];
    [self.person1 setValue:@(29) forKey:@"age"];
    [self.person1 setValue:@(29) forKeyPath:@"age"];
复制代码

2.3.2 手动调用

KVO在属性发生改变时的调用是自动的,若是想要手动控制这个调用时机,或想本身实现KVO属性的调用,则能够经过KVO提供的方法进行调用。bash

下面以age属性为例:app

2.3.2.1 禁用自动调用

//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。工具

2.3.2.2 手动调用实现

- (void)setAge:(NSInteger)age
{
    if (_age != age) {
        [self willChangeValueForKey:@"age"];
        _age = age;
        [self didChangeValueForKey:@"age"];
    }
}
复制代码

实现了(1)禁用自动调用(2)手动调用实现 两步,age属性手动调用就实现了,此时能和自动调用同样,触发KVO。

2.4 移除观察者

- (void)dealloc
{
    [self removeObserver];
}

- (void)removeObserver
{
    [self.person1 removeObserver:self forKeyPath:@"age"];
    [self.person1 removeObserver:self forKeyPath:@"name"];
}
复制代码

2.5 Crash

KVO若使用不当,极容易引起Crash。相关试验代码在KVO-02-crash

2.5.1 观察者未实现监听方法

若观察者对象**-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.

2.5.2 未及时移除观察者

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";
}
复制代码
  1. 假如在当前ViewController中,注册了观察者,点击屏幕,改变被观察对象person1的属性值。

  2. 点击对应按钮,销毁观察者,此时self.observerPersonChange为nil。

  3. 再次点击屏幕,此时Crash;

2.5.3 屡次移除观察者

Cannot remove an observer <ViewController 0x7fc6dc00c090> for the key path "age" from <BFPerson 0x6000014acd00> because it is not registered as an observer.

2.6 keyPath字符串的弊端

在注册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"];
复制代码

2.7 属性依赖

/** 若是age改变 观察者也会收到name改变的通知 */
+ (NSSet<NSString *> *)keyPathsForValuesAffectingAge
{
    NSSet *set = [NSSet setWithObjects:@"name", nil];
    return set;
}
复制代码

3、原理

1. 发现中间对象

为了区分在添加KVO以后,对象以及对应的属性设值方法发生的变化,咱们进行了以下测试:

image-20181126172954638

观察方法实现:

image-20181126173559094

  • 添加KVO先后,person1指向的类对象元类对象,以及setAge:均发生了变化;
  • 添加KVO后,person1中的isa指向了NSKVONotifying_BFPerson类对象;
  • 添加KVO以后,setAge:的实现调用的是:Foundation 中 _NSSetLongLongValueAndNotify方法;

2. 探索调用流程

重写项目在KVO-03-princlipe

(1)重写BFPerson的下列方法

  • - setAge:
  • -willChangeValueForKey:
  • -didChangeValueForKey:

(2)调试

咱们经过重写方法后,进行打印测试

image-20181126230732045

结合

  • 重写打印后的日志:

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

  • 汇编调用栈

image-20181126231110178

  • 中间对象——NSKVONotifying_BFPerson

咱们整理出一个完整的方法链:

  1. self.person1.age = 29;
  2. Foundation _NSSetLongLongValueAndNotify
    1. willChangeValueForKey:
    2. [BFPerson segAge:]
    3. didChangeValueForKey:
      1. -[ViewController observeValueForKeyPath:ofObject:change:context:]

以下图:

image-20181127090036039

可是这些不足以反应真正完整的KVO实现。

3.重现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以后,中间对象的方法列表以及未添加以前的方法列表:

方法列表(忽略name属性相关方法)
BFPerson test, .cxx_destruct, setAge:, age
NSKVONotify_BFPerson setAge:, class, dealloc, _isKVOA
  • isa交换技术

    • 交换以后,调用任何BFPerson对象的方法,都会通过NSKVONotify_BFPerson,可是不一样的方法,有不一样的处理方式。

      • 调用监听的属性设置方法,如 setAge:,都会先调用NSKVONotify_BFPerson对应的属性设置方法;
      • 调用非监听属性设置方法,如test,会经过NSKVONotify_BFPersonsuperclass,找到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类已经成型(相关代码参考项目),结合调用流程,咱们绘制出下面对比图。

(1)未使用KVO对象

image-20181127084920073

(2)使用KVO——生成中间对象

image-20181127084939046

(3)使用KVO——执行流

image-20181127084950233

4、KVC基本使用

项目源码在:KVC-01-usage

(1)常见的API

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 
复制代码

其中,有两个方法要注意:

  • valueForKey与objectForKey的区别
valueForKey objectForKey
无key的处理 无该key,crash,NSUndefinedKeyException 无该key返回nil
来源 KVC主要方法 NSDictionary的方法
符号 若以 @ 开头,去掉 @ ,用剩下部分做为 key 执行 [super valueForKey:] key 不是以 @ 符号开头, 二者等同
  • setValue与setObject的区别
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;
复制代码

(2)集合API

有序集合对应方法以下:

-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>://这两个都是可选的,若是在此类操做上有性能问题,就须要考虑实现之
复制代码

(3)使用场景

a.动态地取值和设值

b.访问和修改私有变量

c.Model和字典转换

d.修改一些控件的内部属性

例如设置: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);
}
复制代码

e.高阶消息传递

当对容器类使用KVC时,valueForKey:将会被传递给容器中的每个对象,而不是容器自己进行操做。结果会被添加进返回的容器中,这样,开发者能够很方便的操做集合来返回另外一个集合。

NSArray *arr = @[@"ali",@"bob",@"cydia"];
NSArray *arrCap = [arr valueForKey:@"capitalizedString"];
for (NSString *str  in arrCap) {
    NSLog(@"%@",str);        //Ali\Bob\Cydia
}
复制代码

f.KVC中的函数操做集合

  • 简单集合运算符

    • @avg
    • @count
    • @max
    • @min
    • @sum
    @interface Book : NSObject
    @property (nonatomic,assign)  CGFloat price;
    @end
    
    NSArray* arrBooks = @[book1,book2,book3,book4];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
    复制代码
  • 对象运算符

    • @distinctUnionOfObjects
    • @unionOfObjects
    // 获取全部Book的price组成的数组,而且去重
    NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
    复制代码
  • Array和Set操做符(集合中包含集合的情形)

    • @distinctUnionOfArrays
    • @unionOfArrays
    • @distinctUnionOfSets

    collection_keypath

5、KVC原理

项目源码在**KVC-02-principle**

  • setValue:forKey

    image-20181127173412899

  • valueForKey

    image-20181127174325478

6、扩展

1._NSSetLongLongValueAndNotify

//抽出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
复制代码

参考

连接

  1. Key-Value Coding Programming Guide
  2. Key-Value Observing Programming Guide
  3. KVC 和 KVO
  4. Key-Value Observing

示例代码

  1. KVC-01-usage
  2. KVC-02-principle
  3. KVO-01-usage
  4. KVO-02-crash
  5. KVO-03-principle
  6. KVOLearnDemo

工具

KVOController Facebook出品的KVO封装库。

相关文章
相关标签/搜索