移动开发技术前沿iOS && AndroidiOS技术点

KVC/KVO 的使用及原理分析

2017-11-14  本文已影响219人  满脸胡茬的小码农

KVC/KVO

概念

KVC / KVO 的使用

其实,我们经常会用到 KVC 和 KVO 机制来解决实际开发中的好多问题,尤其是 KVC,因为这种基于运行时的编程方式大大地提高了灵活性,简化代码;比如,我们经常会用如下代码来修改 textField 的 placeHolder 的字体和颜色等属性。

   [self setValue:[UIColor whiteColor] forKeyPath:@"_placeholderLabel.textColor"];
   [self setValue:[UIFont boldSystemFontOfSize:20] forKeyPath:@"_placeholderLabel.font"];

下面,来为大家列举一下 KVC / KVO 的使用场景

KVC的使用

KVC 的常用的方法:

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

NSKeyValueCoding 类别中的一些其他方法:

+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList([UITableView class], &count);
    NSMutableArray * mutableList_property = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int  i = 0; i < count; i++) {
        const char *propertyName = property_getName(propertyList[I]);
        [mutableList_property addObject:[NSString stringWithUTF8String:propertyName]];
    }
    free(propertyList);
    NSArray * propertylist = [NSArray arrayWithArray:mutableList_property];
    NSLog(@"\n获取UITableView的属性列表:%@",propertylist);
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str  in arrCapStr) {
    NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length  in arrCapStrLength) {
    NSLog(@"%ld",(long)length.integerValue);
}
打印结果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7

KVO 的使用

对于,KVO 的使用,就是观察键值。在 MVC 设计架构中, KVO 适合 在 Model 和 Controller 之间通讯。
使用步骤:

  1. 注册观察者,实施监听
//第一个参数observer:观察者 (这里观察self.myKVO对象的属性变化)

//第二个参数keyPath: 被观察的属性名称(这里观察self.myKVO中num属性值的改变)

//第三个参数options: 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)

//第四个参数context: 上下文,可以为kvo的回调方法传值(例如设定为一个放置数据的字典)

//注册观察者

[self.myKVO addObserver:self forKeyPath:@"num" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; 
  1. 在回调方法中处理属性发生的变化
/keyPath:属性名称

//object:被观察的对象

//change:变化前后的值都存储在change字典中

//context:注册观察者时,context传过来的值

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
}
  1. 移除观察者

注意:
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName",这时是不会触发kvo机制,更加不会调用回调方法的。

实现原理

KVO 是 基于 KVC 实现的。首先了解 KVC 的机制,才能更好的理解 KVO。

KVC 是如何寻找 Key 值的

除了用于有序的可变容器外,mutableArrayValueForKey:一般还用于对 NSMutableArray 添加 Observer 上。如果对象属性是个 可变容器类型时,你给他添加 KVO时,你会发现当你添加或移除元素时并不会接受到变化。因为 KVO 的本质是系统监测到某个属性的内存地址或者常量改变时,会添加上- (void)willChangeValueForKey:(NSString *)key- (void)didChangeValueForKey:(NSString *)key方法来发送通知,所以一种方法是手动调用这两个方法,一种是使用上述的 mutableArrayValueForKey: 方法。
对于无序容器时,可以使用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
该方法返回一个可变的无序数组,如果调用该方法,KVC 的搜索顺序除了检查 receiver 是 ManagedObject 以外,其搜索顺序和 mutableArrayValueForKey 基本一致。

KVO 的原理

我们之前就说了 KVO 的实现依靠的是 Objective-C 强大的 Runtime。那么具体的,我们是如何实现这一机制的呢?下面让我们共同来探讨一下。

基本原理:
当观察对象 A 时, KVO 机制动态的创建了一个对象 A 当前的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法。在 setter 方法中,我们通知观察对象属性的改变状态。

进一步剖析:
其实 Apple 使用了 isa 混写,即 isa-swizzling 来实现 KVO。

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.

那么,我们通过下面的代码来看看,isa-swizzling 的真面目到底是什么?

// 在 PQCPeople.m 中,我们模拟 KVO 的过程,打印 isa 指针指向的类,以及 setter 方法的的函数指针
- (void)printUserInfo {
    NSLog(@"isa:%@,supperclass:%@",NSStringFromClass(object_getClass(self)),
          class_getSuperclass(object_getClass(self)));
    NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
    NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setNamestr:)));
    NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printUserInfo)));
}

在运行过程中,在添加Observer前,添加Observer以及删除Observer后分别打印出该类的信息。

    PQCPeople *person = [[PQCPeople alloc]init];
    NSLog(@"Before add observer————————————————————————–");
    [person printUserInfo];
    [person addObserver:self forKeyPath:@"namestr" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"After add observer————————————————————————–");
    [person printUserInfo];
    [person removeObserver:self forKeyPath:@"namestr"];
    NSLog(@"After remove observer————————————————————————–");
    [person printUserInfo];

以下是打印的信息:

2017-11-14 13:06:51.927605+0800 KVC&KVO[851:92433] Before add observer————————————————————————–
2017-11-14 13:06:51.927701+0800 KVC&KVO[851:92433] isa:PQCPeople,supperclass:NSObject
2017-11-14 13:06:51.927814+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.927904+0800 KVC&KVO[851:92433] name setter function pointer:0x10c2429d0
2017-11-14 13:06:51.927987+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0
2017-11-14 13:06:51.928224+0800 KVC&KVO[851:92433] After add observer————————————————————————–
2017-11-14 13:06:51.928316+0800 KVC&KVO[851:92433] isa:NSKVONotifying_PQCPeople,supperclass:PQCPeople
2017-11-14 13:06:51.928489+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.928628+0800 KVC&KVO[851:92433] name setter function pointer:0x10c58f666
2017-11-14 13:06:51.928798+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0
2017-11-14 13:06:51.928954+0800 KVC&KVO[851:92433] After remove observer————————————————————————–
2017-11-14 13:06:51.929077+0800 KVC&KVO[851:92433] isa:PQCPeople,supperclass:NSObject
2017-11-14 13:06:51.929268+0800 KVC&KVO[851:92433] self:<PQCPeople: 0x604000059c50>, [self superclass]:NSObject
2017-11-14 13:06:51.929431+0800 KVC&KVO[851:92433] name setter function pointer:0x10c2429d0
2017-11-14 13:06:51.929586+0800 KVC&KVO[851:92433] printInfo function pointer:0x10c242ba0

通过分析,我们会发现在添加KVO之后,isa 已经替换成了NSKVONotifying_PQCPeople,而根据 class_getSuperclass得到的结果竟然是 PQCPerson, 然后 namestr 是我们KVO需要观察的属性,它的 setter函数指针也变了。
我们上面也说道, OC 的消息机制是通过 isa 去查找实现的,那么我们可以根据以上的分析,可以大致得出,KVO的实现应该是:

因此,我们的 KVO 就是通过如图这种过程,进行键值观察。


isa_swizzling.jpg

文末

通过上面的分析,我们也大概了解了 KVC 以及 KVO 的实现过程以及实现原理,对这个感兴趣的同学,我们可以试着去自己实现一下 KVC 以及 KVO。

上一篇下一篇

猜你喜欢

热点阅读