iOS中的KVC与KVO

KVC

KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法

KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject),下面是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,_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 *)dictionaryWithValuesForKeys:(NSArray *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

KVC的相关概念

KVC的赋值

KVC如果要赋值的话首先就需要对象对应的Key,那么KVC的内部是怎么工作的呢,当我们使用setValue:forKey:的代码时,底层的执行机制顺序如下:

1.程序首先通过setter方法进行赋值,比如我们的key@"name",则会调用setName:方法。

2.如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法。

3.如果我们没有设置accessInstanceVariablesDirectly返回NOKVC机制会搜索该类里面有没有名为name的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只要存在以name命名的变量KVC都可以对该成员变量赋值。

4.如果该类即没有setName:方法,也没有_name成员变量,KVC机制会搜索_isName的成员变量。

5.和上面一样,如果该类即没有setName:方法,也没有_name_isName成员变量,KVC机制再会继续搜索nameisName的成员变量。再给它们赋值。

6.如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

让我们用代码一一来说明:

//test类中声明成员变量
#import "testKVC.h"

@interface testKVC()
{
    NSString *_name;
}
@end

@implementation testKVC

@end

//执行文件中为_name赋值
testKVC *obj = [[testKVC alloc] init];
[obj setValue:@"MichealMIX" forKey:@"name"];
NSLog(@"obj的成员变量name的值为:%@",[obj valueForKey:@"name"]);

//控制台输出
2020-01-09 20:28:55.576794+0800 KVC&KVO[8003:713495] obj的成员变量name的值为:MichealMIX

通过- (void)setValue:(nullable id)value forKey:(NSString *)key;- (nullable id)valueForKey:(NSString *)key;成功设置和取出obj对象的name值。

再看一下设置accessInstanceVariablesDirectlyNO的效果:

#import "testKVC.h"

@interface testKVC()
{
    NSString *_name;
}
@end

@implementation testKVC

+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

//取值出现错误
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,名为%@的key不存在",key);
    return nil;
}

//赋值出现错误
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,名为%@的key不存在", key);
}

@end

//控制台输出
2020-01-09 20:35:56.716841+0800 KVC&KVO[8082:718165] 出现异常,名为name的key不存在
2020-01-09 20:35:56.717084+0800 KVC&KVO[8082:718165] 出现异常,名为name的key不存在
2020-01-09 20:35:56.717201+0800 KVC&KVO[8082:718165] obj的成员变量name的值为:(null)

可以看到如果accessInstanceVariablesDirectly返回值设置为NO,那么只会查找gettersetter这一层,而不会继续寻找。

我们将accessInstanceVariablesDirectly设置为YES,然后设置成员变量名字为isName,看看能否成功赋值取值。

#import "testKVC.h"

@interface testKVC()
{
    NSString *isName;
}
@end

@implementation testKVC

+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//取值出现错误
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,名为%@的key不存在",key);
    return nil;
}

//赋值出现错误
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,名为%@的key不存在", key);
}

@end

//控制台输出
2020-01-09 20:51:31.572195+0800 KVC&KVO[8157:723618] obj的成员变量name的值为:MichealMIX

KVC的取值

当调用valueForKey:的代码时,KVCkey的搜索方式不同于setValue:forKey:,我们假设成员变量为age,按getAge,age,isAge的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

代码来验证一下:

#import "testKVC.h"

@interface testKVC()

@end

@implementation testKVC

- (NSUInteger)getAge {
    return 10;
}

@end

//调用取值方法
testKVC *obj = [[testKVC alloc] init];
NSLog(@"年龄的值为:%@",[obj valueForKey:@"age"]);
//控制台输出
2020-01-10 09:39:05.558566+0800 KVC&KVO[8628:742152] 年龄的值为:10

再来验证一下ageisAge的取值方式:

#import "testKVC.h"

@interface testKVC()

@end

@implementation testKVC

- (NSUInteger)age {
    return 10;
}
//或者
- (NSUInteger)isAge {
    return 10;
}

@end

//调用取值方法
testKVC *obj = [[testKVC alloc] init];
NSLog(@"年龄的值为:%@",[obj valueForKey:@"age"]);
//控制台输出
2020-01-10 09:39:05.558566+0800 KVC&KVO[8628:742152] 年龄的值为:10

这些方式都可以找到age的值,也就验证了我们之前的定义。

KeyPath

一个类的成员变量有可能是自定义类或其他的复杂数据类型,你可以先用KVC获取该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径keyPath。顾名思义,就是按照路径寻找key

先看一下代码:

//定义一个test类,含有成员变量name
#import "test.h"

@interface test()
{
    NSString *name;
}
@end

@implementation test

@end

//再定义一个testKVC类,其含有成员变量test
#import "testKVC.h"
#import "test.h"
@interface testKVC()
{
    test*testKeyPath;
}
@end

@implementation testKVC

@end

//调用函数中为test类中的name赋值
testKVC *obj = [[testKVC alloc] init];
test *obj1 = [[test alloc] init];
[obj setValue:obj1 forKey:@"testKeyPath"];
[obj setValue:@"MichealMIX" forKeyPath:@"testKeyPath.name"];
    
 NSLog(@"test的成员变量name的值为:%@",[obj valueForKeyPath:@"testKeyPath.name"]);
//控制台输出
2020-01-10 10:12:39.293053+0800 KVC&KVO[9066:762997] test的成员变量name的值为:MichealMIX

KVC对于keyPath是搜索机制第一步就是分离key用小数点.来分割key,然后再像普通key一样按照先前介绍的顺序搜索下去。

KVC处理异常

KVC处理nil

通常情况KVC是不允许你在赋值的时候传nil的,我们可以重写一个方法,防止在不小心的情况下误传了nil造成不必要的问题。

//testKVC类
#import "testKVC.h"
@interface testKVC()
{
    NSUInteger age;
}
@end

@implementation testKVC
//重写方法
- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}

@end

//调用赋值函数
testKVC *obj = [[testKVC alloc] init];
[obj setValue:nil forKey:@"age"];
//控制台输出
2020-01-10 10:42:58.180137+0800 KVC&KVO[9307:777429] 不能将age设成nil

KVC键值验证

我们可以使用validateValue来验证赋值的合法性,比如一些变量的值要大于0,我们就可以使用键值验证。

#import "testKVC.h"
@interface testKVC()
{
    NSUInteger age;
}
@end

@implementation testKVC

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue < 0) {
        return NO;
    }
    
    return YES;
}

@end

//调用方法
testKVC *obj = [[testKVC alloc] init]; 
NSNumber *age = [NSNumber numberWithLong:-1];
NSError* error;
NSString *key = @"age";
BOOL isValid = [obj validateValue:&age forKey:key error:&error];
    
    if (isValid) {
        NSLog(@"赋值合法");
        [obj setValue:age forKey:key];
    }
    else {
        NSLog(@"赋值不合法");
    }
    //通过KVC取值age打印
    NSLog(@"test的年龄是%@", [obj valueForKey:@"age"]);

//控制台输出
2020-01-10 10:54:30.785211+0800 KVC&KVO[9397:783600] 赋值不合法
2020-01-10 10:54:30.785501+0800 KVC&KVO[9397:783600] test的年龄是0

KVC简单的集合处理

简单集合运算符共有@avg, @count , @max , @min ,@sum5种,从字义就能看出来他们分别代表什么了,不支持自定义。

//Person类
@interface Person : NSObject

@property(nonatomic,strong)NSString *name;

@property(nonatomic,assign)int age;

@end

#import "Person.h"

@implementation Person

@end

//调用函数
- (void)main {

    Person *tom = [[Person alloc] init];
    tom.name = @"Tom";
    tom.age = 20;
    
    Person *jim = [[Person alloc] init];
    jim.name = @"Jim";
    jim.age = 10;
    
    Person *steve = [[Person alloc] init];
    steve.name = @"Steve";
    steve.age = 15;
    
    Person *james = [[Person alloc] init];
    james.name = @"James";
    james.age = 30;
    
    Person *jane = [[Person alloc] init];
    jane.name = @"Jane";
    jane.age = 33;
    
    NSArray* arrBooks = @[tom,jim,steve,james,jane];
    NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.age"];
    NSLog(@"sum:%d",sum.intValue);
    NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.age"];
    NSLog(@"avg:%d",avg.intValue);
    NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
    NSLog(@"count:%d",count.intValue);
    NSNumber* min = [arrBooks valueForKeyPath:@"@min.age"];
    NSLog(@"min:%d",min.intValue);
    NSNumber* max = [arrBooks valueForKeyPath:@"@max.age"];
    NSLog(@"max:%d",max.intValue);

}

//控制台输出
2020-01-10 11:17:01.715837+0800 KVC&KVO[9616:797516] sum:108
2020-01-10 11:17:01.716080+0800 KVC&KVO[9616:797516] avg:21
2020-01-10 11:17:01.716224+0800 KVC&KVO[9616:797516] count:5
2020-01-10 11:17:01.716345+0800 KVC&KVO[9616:797516] min:10
2020-01-10 11:17:01.716456+0800 KVC&KVO[9616:797516] max:33

KVC处理字典

KVC中关于处理字典的两个方法:

- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

我们通过代码来看一下这些方法会更加直观:

//模型类
@interface Address : NSObject

@property (nonatomic, copy)NSString* country;
@property (nonatomic, copy)NSString* province;
@property (nonatomic, copy)NSString* city;
@property (nonatomic, copy)NSString* district;

@end

//运行函数
- (void)main {
    Address *address = [[Address alloc] init];
    address.country = @"China";
    address.province = @"Guang Dong";
    address.city = @"ShenZhen";
    address.district = @"Nanshan";
    
    NSArray *adressArray = @[@"country",@"province",@"city",@"district"];
    
    //将模型转换为字典
    NSDictionary *dict = [address dictionaryWithValuesForKeys:adressArray];
    
    NSLog(@"KVC建立的字典%@",dict);
    
    //字典转模型
    NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
    [address setValuesForKeysWithDictionary:modifyDict];   //用key Value来修改Model的属性
    NSLog(@"country:%@  province:%@ city:%@",address.country,address.province,address.city);

}
//控制台输出
2020-01-10 13:54:19.101262+0800 KVC&KVO[9883:816765] KVC建立的字典{
    city = ShenZhen;
    country = China;
    district = Nanshan;
    province = "Guang Dong";
}
2020-01-10 13:54:19.101446+0800 KVC&KVO[9883:816765] country:USA  province:california city:Los angle

KVC的使用

1.动态的取值和赋值
2.用KVC来访问或者修改私有变量
3.model和字典的互相转换
4.修改一些控件的内部属性

KVO

KVO就是基于之前的KVC来实现的,我们来看看什么是KVOKVO 即 Key-Value Observing,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察者,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,自动的通知观察者

简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。KVO的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueObserving类别名,和KVC一样swift等类无法使用。

首先我们通过代码简单看一下KVO是如何实现的:

//定义一个类,在.h文件声明一个成员变量及其存取方法
@interface TestKVO : NSObject
{
    int number;
}

- (void)setNumber:(int)theNum;
- (int)number;

@end

//.m文件
#import "TestKVO.h"

@implementation TestKVO

- (instancetype)init{
    self = [super init];
    if (self != nil) {
        //初始化number为100
        number = 100;
    }
    return self;
}

- (void)setNumber:(int)theNum{
    number = theNum;
}

- (int)number{
    return number;
}

@end

//调用函数

- (void)main{
    kvo = [[TestKVO alloc] init];
  
    [kvo addObserver:self forKeyPath:@"number" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];

    [kvo setNumber:150];
}
//回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"change = %@",change);
    if ([keyPath isEqualToString:@"number"] && object == kvo) {
        
    }
}

//移除观察者
- (void)dealloc {
    [kvo removeObserver:self forKeyPath:@"number" context:nil];
}

//控制台打印
2020-01-10 15:24:12.228724+0800 KVC&KVO[10498:854550] change = {
    kind = 1;
    new = 150;
    old = 100;
}

可以看到KVO成功的监听了成员变量number的值,并且返回给我们旧值以及改变后的值。

首先我们想使用KVO一定要了解一下两个方法:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

我们要为想观察的对象添加观察者,当完成观察后一定要移除观察者,否则会造成内存泄漏

再来看一下这两个方法中的参数以及重要的回调方法:

observer:观察者,也就是KVO通知的订阅者。必须实现 
observeValueForKeyPath:ofObject:change:context:方法,它会返回给我们记录前后变化的字典
keyPath:描述将要观察的属性,相对于被观察者。
options:KVO的一些属性配置;有四个选项。
context: 上下文,这个会传递到订阅着的函数中,用来区分消息,所以应当是不同的。

options中的内容:

NSKeyValueObservingOptionNew:change字典包括改变后的值
NSKeyValueObservingOptionOld:change字典包括改变前的值
NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)

这里还要提到一点,有一些情况下我们需要手动进行KVO,比如一些类库不希望我们KVO,这时候就又有两个重要的方法出现了:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

KVO的自动实现就是隐式的调用了这两个方法来帮我们记录值的变化,并且它会进行通知,而手动实现KVO就需要我们显示的调用了,由于我们手动调用这两个方法,那我们就要将值发生变化的自动通知关掉。

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

来看一下实现手动KVO的代码:

//在刚才的类中新增一个age成员变量
@interface TestKVO : NSObject
{
    int age;
    int number;
}

- (void)setAge:(int)theAge;
- (int)age;

- (void)setNumber:(int)theNum;
- (int)number;

@end

//.m
#import "TestKVO.h"

@implementation TestKVO

- (instancetype)init{
    self = [super init];
    if (self != nil) {
        age = 10;
        number = 100;
    }
    return self;
}

- (void)setAge:(int)theAge{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

- (int)age{
    return age;
}

- (void)setNumber:(int)theNum{
    number = theNum;
}

- (int)number{
    return number;
}

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    //关闭对键age的自动通知
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    //其他照旧
    return [super automaticallyNotifiesObserversForKey:key];
}

@end

//调用函数
- (void)main{
    kvo = [[TestKVO alloc] init];
    
    [kvo addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
  
    [kvo setAge:15];
}
//控制台打印
2020-01-10 16:16:17.990038+0800 KVC&KVO[10737:871671] change = {
    kind = 1;
    new = 15;
    old = 10;
}

可以看到,我们关闭自动KVO,手动执行变化通知也可以打印出记录变化的结果,不要忘记要移除观察者,以免内存泄漏。

注意!

一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop的处理。手动或者自动调用-didChange... 会触发 KVO 通知。

所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用KVO

KVO的实现

KVO 是通过isa-swizzling实现的。
编译器自动为被观察对象创造一个派生类,并将被观察对象的isa指向这个派生类。如果用户注册了对此目标对象的某一个属性的观察,那么此派生类会重写setter方法,并在其中添加进行通知的代码Objective-C在发送消息的时候,会通过isa指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法

KVO的原理是修改 setter 方法,因此使用KVO 必须调用 setter若直接访问属性对象则没有效果

参考文章:https://www.jianshu.com/p/b9f020a8b4c9

你可能感兴趣的:(iOS中的KVC与KVO)