【iOS】——KVC和KVO

KVC

KVC定义

KVC(Key-value coding)键值编码,允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。

也就是说它提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法访问。KVO 就是基于 KVC 实现的关键技术之一。

KVC的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC

KVC常用API

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;//通过keyPath设置值
- (void)setValue:(id)value forKey:(NSString *)key;//通过key设置值
- (id)valueForKeyPath:(NSString *)keyPath;//通过keyPath获取值
- (id)valueForKey:(NSString *)key;//通过key获取值

  • key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: “frame”);

  • keypath:除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: “layer.cornerRadius”);

KVC其它API

+ (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转到字典。

KVC原理

设值

setValue:forKey: 的原理

  • 首先会按照setKey_setKey的顺序查找方法,找到方法,直接调用方法并赋值;

  • 未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly(是否可以直接访问成员变量,默认返回YES);

  • accessInstanceVariablesDirectly方法返回YES,则按照_key_isKeykeyisKey的顺序查找成员变量,找到直接赋值,找不到则抛出NSUnknowKeyExpection异常;

  • accessInstanceVariablesDirectly方法返回NO,那么就会调用setValue:forUndefinedKey:并抛出NSUnknowKeyExpection异常;

在这里插入图片描述

valueForKey:的原理

  • 首先会按照getKeykeyisKey_key的顺序查找方法,找到直接调用取值

  • 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出NSUnknowKeyExpection异常;

  • 若返回的YES,则按照_ key_isKeykeyisKey的顺序查找成员变量,找到则取值;

  • 找不到则调用valueForUndefinedKey:抛出NSUnknowKeyExpection异常;

在这里插入图片描述

如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set:属性名时,会直接用setValue:forUndefinedKey:方法。

使用keyPath

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

KVC 允许您通过指定属性路径来获取或设置对象的属性值。例如,如果您有一个对象 person,并且想访问 personaddress 对象中的 street 属性,可以使用以下语法:

NSString *street = [person valueForKeyPath:@"address.street"];

KVC处理异常

KVC处理nil异常

通常情况下,KVC不允许你要在调用setValue: forKey:(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
    NSUInteger age;
}

@end

@implementation Test

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能将%@设成nil", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成对象
        Test *test = [[Test alloc] init];
        //通过KVC设值test的age
        [test setValue:nil forKey:@"age"];
        //通过KVC取值age打印
        NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

KVC处理UndefinedKey异常

通常情况下,KVC不允许你要在调用setValue: forKey:(或者keyPath)时对不存在的key进行操作。 不然,会调用forUndefinedKey给出异常,重写forUndefinedKey方法避免崩溃。

#import <Foundation/Foundation.h>

@interface Test: NSObject {
}

@end

@implementation Test

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出现异常,该key不存在%@", key);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //Test生成对象
        Test *test = [[Test alloc] init];
        //通过KVC设值test的age
        [test setValue:@10 forKey:@"age"];
        //通过KVC取值age打印
        NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
        
    }
    return 0;
}

KVC键值验证(Key-Value Validation)

KVC提供了验证Key对应的Value是否可用的方法:

- (BOOL)validateValue:(inoutid*)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;

该方法默认的实现是调用一个如下格式的方法:

- (BOOL)validate<Key>:error:

KVC是不会自动调用键值验证方法的,我们如果想要键值验证则需要手动验证

KVC处理字典

批量存值操作

KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

批量赋值操作

通过KVC可以使用对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包好keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给对象的属性赋值。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

下面是个例子:

@interface Person : NSObject
@property (nonatomic, copy)NSString* name;
@property (nonatomic, assign)NSInteger age;
@property (nonatomic, copy)NSString* sex;
@end

@implementation Person

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        Person* person = [[Person alloc] init];
        [person setValue:@"Bill" forKey:@"name"];
        [person setValue:@"12" forKey:@"age"];
        [person setValue:@"male" forKey:@"sex"];
        NSDictionary* firstDictionary = [person dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
        NSLog(@"dictonary = %@", firstDictionary);
        NSDictionary* secondDictionary = @{@"name":@"Danny", @"age":@12, @"sex": @"female"};
        Person* secondPerson = [[Person alloc] init];
        [secondPerson setValuesForKeysWithDictionary:secondDictionary];
        NSLog(@"name  = %@, age = %ld, sex = %@", secondPerson.name, secondPerson.age, secondPerson.sex);
        
    }
    return 0;
}

在这里插入图片描述

字典和模型转换

@interface Address()

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

@end

@implementation Address

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //模型转字典
        Address* add = [Address new];
        add.country = @"China";
        add.province = @"Guang Dong";
        add.city = @"Shen Zhen";
        add.district = @"Nan Shan";
        NSArray* arr = @[@"country",@"province",@"city",@"district"];
        NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把对应key所有的属性全部取出来
        NSLog(@"%@",dict);
        
        //字典转模型
        NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
        [add setValuesForKeysWithDictionary:modifyDict];            //用key Value来修改Model的属性
        NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);
        
        
    }
    return 0;
}

用KVC实现高阶消息传递

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

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        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);
        }
        
    }
    return 0;
}

方法capitalizedString被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString并返回一个包含结果的新的NSArray。 从打印结果可以看出,所有String都成功以转成了大写。 同样如果要执行多个方法也可以用valueForKeyPath:方法。它先会对每一个成员调用 capitalizedString方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。

在这里插入图片描述

KVO

KVO的全称是KeyValueObserving,俗称“键值监听",可以用于监听某个对象属性值的改变;KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。

基本思想:对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。

KVO是苹果提供的在套事件通知机制。KVO和NSNotificationCenter都是iOS中观察者模式的一种实现,区别是:NSNotificationCenter可以是一对多的关系,而KVO是一对一的;

KVO使用

注册KVO监听

通过[addObserver:forKeyPath:options:context:]方法注册KVO,这样可以接收到keyPath属性的变化事件

  • observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。

  • keyPath:要观察的属性名称。要和属性声明的名称一致。

  • options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容

  • context:上下文,这个会传递到观察者的函数中,用来区分消息,所以应当是不同的。

options所包括的内容:

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

实现KVO监听

通过方法[observeValueForKeyPath:ofObject:change:context:]实现KVO的监听

  • keyPath:被观察对象的属性
  • object:被观察的对象
  • change:字典,存放相关的值,根据options传入的枚举来返回新值旧值
  • context:注册观察者的时候,context传递过来的值

移除KVO监听

通过方法[removeObserver:forKeyPath:],移除监听;

处理变更通知

每当监听的keyPath发生变化了,就会在这个函数中回调:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions

手动KVO(禁用KVO)

KVO的实现是对注册的keyPath的setter方法中,自动插入并调用了两个函数。

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

手动实现KVO先需要关闭自动生成KVO通知,然后手动的调用。手动通知的好处就是,可以灵活加上自己想要的判断条件。

关闭KVO通知需要调用下面函数:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

接着手动实现属性的 setter 方法,在setter方法中先调用willChangeValueForKey:接着进行赋值操作,然后调用willChangeValueForKey:

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

KVO和线程

KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Runloop 的处理。手动或者自动调用 -didChangeValueForKey: 会触发 KVO 通知。

KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:

  • 保证所有监听某一属性的观察者在setter方法返回前被通知到
  • 如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到observeValueForKeyPath:ofObject:change:context:被调用之前,监听的属性都会返回同样的值

KVO实现

KVO 是通过 isa-swizzling 实现的

基本流程

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

注意派生类只重写注册了观察者的属性方法。

即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相比较于ObjectA,会重写以下几个方法:

  • setter方法
  • class方法
  • delloc方法
  • _isKVOA方法

NSKVONotifyin_Person的内存结构以及方法调用顺序

重写setter方法

在 setter 中,会添加以下两个方法的调用:

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

然后在 didChangeValueForKey: 中,去调用:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context;

包含了新值和旧值的通知

重写class

这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错

- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
    return class_getSuperclass(object_getClass(self));
}

当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface ObjectA: NSObject

@property (nonatomic) NSInteger age;

@end

@implementation ObjectA
@end

@interface ObjectB: NSObject
@end

@implementation ObjectB

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        //生成对象
        ObjectA *objA = [[ObjectA alloc] init];
        ObjectB *objB = [[ObjectB alloc] init];
        
        // 添加Observer之后
        [objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        
        // 输出ObjectA
        NSLog(@"%@", [objA class]);
        // 输出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)
        NSLog(@"%@", object_getClass(objA));

    }
    return 0;
}

object_getClass()函数可以返回对象的isa指向的实际类(class),而不是对象所属的类的类型。class方法返回的是对象所属的类的类型

在这里插入图片描述

重写dealloc

系统重写 dealloc 方法来释放资源。

重写_isKVOA

判断这个类有没有被KVO动态生成子类

总结

KVC

  • key的值必须正确,如果拼写错误,会出现异常。

  • 当key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来。

  • 因为类可以反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去。

  • NSArray/NSSet等都支持KVC。

  • 可以通过KVC访问自定义类型的私有成员。

  • 如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法,我们可以重写这个方法来避免传递nil出现的错误,对象并不会调用这个方法,而是会直接报错。

  • 处理非对象,setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型,valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValue。valueForKey可以自动将值封装成对象,但是setValue:forKey:却不行。我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递initWithBool:(BOOL)value。

KVO

  • 调用[removeObserver:forKeyPath:]需要在观察者消失之前,否则会导致Crash。

  • 在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash。

  • 观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash。

  • KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。

  • 在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。

问题总结

直接修改成员变量的值,会不会触发KVO?

不会触发KVO,KVO的本质是替换了setter方法的实现,所以只有通过set方法修改才会触发KVO。

KVC修改属性会触发KVO吗?

会的 ,尽管setvalue:forkey:方法不一定会触发instance实例对象的setter:方法,但是setvalue:forkey:在更改成员变量值的时候,会调用willchangevalueforkeydidchangevalueforkey,会触发监听器的回调方法。

KVO怎么监听数组的元素变化?

KVO默认只能监听到数组对象本身的变化,而无法监听到数组内部元素的变化。例如,如果将一个对象添加到数组中,KVO会收到数组count属性的变化通知,但不会收到数组内部元素的变化通知。

当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:

使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项

KVO支持使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项,来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,根据KVO通知中的信息来处理数组元素的变化。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"myArray"]) {
        NSArray *oldArray = change[NSKeyValueChangeOldKey];
        NSArray *newArray = change[NSKeyValueChangeNewKey];
        // 处理数组元素的变化
    }
}

这种方式需要被观察的对象的数组属性必须是可变的,而且只能监听到元素的增加、删除和替换操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值