iOS KVO和KVC详解

KVC

KVC定义

KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的。

在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用。但是没有访问起方法的类中,点语法无法使用,这时KVC就有优势了。

1.设置器和访问器的定义

*   给单一实例变量赋值的方法叫做设置器.(setter方法)

*   给单一实例变量值的方法叫做访问器.(getter方法)

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来设值

KVC设值

KVC要设值,那么就要对象中对应的key,KVC在内部是按什么样的顺序来寻找key的。当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:

程序优先调用set:属性值方法,代码通过setter方法完成设置。注意,这里的是指成员变量名,首字母大小写要符合KVC的命名规则,下同

如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为_的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以_命名的变量,KVC都可以对该成员变量赋值。

如果该类即没有set:方法,也没有_成员变量,KVC机制会搜索_is的成员变量。

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

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

简单来说就是如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。

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

#import

@interface Test: NSObject {

    NSString *_name;

}

@end

@implementation Test

@end

int main(int argc, const char * argv[]) {

    @autoreleasepool {

        // insert code here...


        //生成对象

        Test *obj = [[Test alloc] init];

        //通过KVC赋值name

        [obj setValue:@"xiaoming" forKey:@"name"];

        //通过KVC取值name打印

        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);


    }

    return 0;

}

打印结果: 2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] obj的名字是xiaoming

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

再看一下设置accessInstanceVariablesDirectly为NO的效果:

#import

@interface Test: NSObject {

    NSString *_name;

}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {

    return NO;

}

- (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 *obj = [[Test alloc] init];

        //通过KVC赋值name

        [obj setValue:@"xiaoming" forKey:@"name"];

        //通过KVC取值name打印

        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);

    }

    return 0;

}

打印结果:

2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] 出现异常,该key不存在name

2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] 出现异常,该key不存在name

2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj的名字是(null)

可以看到accessInstanceVariablesDirectly为NO的时候KVC只会查询setter和getter这一层,下面寻找key的相关变量执行就会停止,直接报错。

设置accessInstanceVariablesDirectly为YES,再修改_name为_isName,看看执行是否成功。

#import

@interface Test: NSObject {

    NSString *_isName;

}

@end

@implementation Test

+ (BOOL)accessInstanceVariablesDirectly {

    return YES;

}

- (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 *obj = [[Test alloc] init];

        //通过KVC赋值name

        [obj setValue:@"xiaoming" forKey:@"name"];

        //通过KVC取值name打印

        NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);

    }

    return 0;

}

打印结果:

2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] obj的名字是xiaoming

从打印可以看到设置accessInstanceVariablesDirectly为YES,KVC会继续按照顺序查找,并成功设值和取值了。

当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:

首先按get,,is的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。

KVC处理异常

KVC中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。

KVC处理nil异常

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

#import

@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

@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;

}

打印结果:

2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age

2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] 出现异常,该key不存在age

2018-05-05 16:30:18.565216+0800 KVCKVO[35487:6277523] test的年龄是(null)

KVC处理数值和结构体类型属性

不是每一个方法都返回对象,但是valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。

这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开以者需要手动转换成原来的类型。

尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。

因为传递进去和取出来的都是id类型,所以需要开发者自己担保类型的正确性,运行时Objective-C在发送消息的会检查类型,如果错误会直接抛出异常。

举个例子,Person类有个NSInteger类型的age属性,如下:

// Person.m

#import "Person.h"

@interface Person ()

@property (nonatomic,assign) NSInteger age;

@end

@implementation Person

@end

修改值

我们通过KVC技术使用如下方式设置age属性的值:

[person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];

我们赋给age的是一个NSNumber对象,KVC会自动的将NSNumber对象转换成NSInteger对象,然后再调用相应的访问器方法设置age的值。

获取值

同样,以如下方式获取age属性值:

[person valueForKey:@"age"];

这时,会以NSNumber的形式返回age的值。

需要注意的是我们不能直接将一个数值通过KVC赋值的,我们需要把数据转为NSNumber和NSValue类型传入,那到底哪些类型数据要用NSNumber封装哪些类型数据要用NSValue封装呢?看下面这些方法的参数类型就知道了:

可以使用NSNumber的数据类型有:就是一些常见的数值型数据。

可以使用NSValue的数据类型有:NSValue主要用于处理结构体型的数据,它本身提供了如上集中结构的支持。任何结构体都是可以转化成NSValue对象的,包括其它自定义的结构体。

KVC使用

动态地取值和设值

利用KVC动态的取值和设值是最基本的用途了。

用KVC来访问和修改私有变量

对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。

Model和字典转换

这是KVC强大作用的又一次体现,KVC和Objc的runtime组合可以很容易的实现Model和字典的转换。

修改一些控件的内部属性

这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API,这样我们就无法正常地访问和修改这些控件的样式。

而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。

KVO

KVO定义

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

观察者模式是什么

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。KVO的定义都是对NSObject的扩展来实现的,Objective-C中有个显式的NSKeyValueObserving类别名,所以对于所有继承了NSObject的类型,都能使用KVO(一些纯Swift类和结构体是不支持KVC的,因为没有继承NSObject)。

KVO使用

注册与解除注册

如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察对象)上调用名为 NSKeyValueObserverRegistration 的 category 方法将观察者对象与被观察者对象注册与解除注册:

- (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决定了是否在改变前改变后通知两次)

这两个方法在手动实现键值观察时会用到。注意在不用的时候,不要忘记解除注册,否则会导致内存泄露。

处理变更通知

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

- (void)observeValueForKeyPath:(NSString *)keyPath

                      ofObject:(id)object

                        change:(NSDictionary *)change

                      context:(void *)context

手动KVO(禁用KVO)

KVO的实现,是对注册的keyPath中自动实现了两个函数,在setter中,自动调用。

- (void)willChangeValueForKey:(NSString *)key

- (void)didChangeValueForKey:(NSString *)key

键值观察依赖键

有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。

KVO和线程

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

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

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

KVO的实现依赖于Runtime的强大动态能力。

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

重写setter

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

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

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

然后在 didChangeValueForKey: 中,去调用:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath

                      ofObject:(nullable id)object

                        change:(nullable NSDictionary *)change

                      context:(nullable void *)context;

包含了新值和旧值的通知。

于是实现了属性值修改的通知。因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter 。若直接访问属性对象则没有效果。

注:在我们日常开发中,我们一般取这个Options值为NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这时,当变量值被修改时,我们在change字典中既能得到新值,也能得到修改之前的值

PS:使用时,addObserver与removeObserver需要成对出现,在销毁对象时移除监听。

#import "QiCompany.h"

@interface QiCompany()

@property (nonatomic, strong) NSString *addr;

@end

@implementation QiCompany

- (void)aboutKVO {

    self.staff = [[QiStaff alloc] init];

    self.staff.staffId = @"1000119";

    self.staff.staffName = @"佩奇";

    [self.staff addObserver:self forKeyPath:@"staffId" options:NSKeyValueObservingOptionNew context:nil];

    [self.staff addObserver:self forKeyPath:@"staffName" options:NSKeyValueObservingOptionNew context:nil];

    self.staff.staffId = @"1000120";

    self.staff.staffName = @"佩德罗";

}

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

    NSLog(@"keyPath = %@  object=%@  newValue=%@  context=%@", keyPath, object, [change objectForKey:@"new"], context);

}

- (void)dealloc {

    [self.staff removeObserver:self forKeyPath:@"staffId"];

    [self.staff removeObserver:self forKeyPath:@"staffName"];

}

@end

日志输出:

2018-11-13 16:25:21.215872+0800 QiKVO&KVC[20986:895803] keyPath = staffId object= newValue=1000120 context=(null)

2018-11-13 16:25:21.216040+0800 QiKVO&KVC[20986:895803] keyPath = staffName object= newValue=佩德罗 context=(null)

总结

KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法

系统会动态创建一个继承于 Person 的 NSKVONotifying_Person

person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法

重写NSKVONotifying_Person的setter方法:[super setName:newName]

通知观察者告诉属性改变。

KVO给我们提供了更少的代码,和比NSNotification好处,不需要修改被观察的class, 永远都是观察你的人做事情。 但是KVO也有些毛病, 1. 如果没有observer监听key path, removeObsever:forKeyPath:context: 这个key path, 就会crash, 不像NSNotificationCenter removeObserver。 2. 对代码你很难发现谁监听你的property的改动,查找起来比较麻烦。 3. 对于一个复杂和相关性很高的class,最好还是不要用KVO, 就用delegate 或者 notification的方式比较简洁。

你可能感兴趣的:(iOS KVO和KVC详解)