[Objective-C] KVC 和 KVO

KVC Key Value Coding

KVC是一种用间接方式访问类的属性的机制。比如你要给一个类中的属性赋值或者取值,可以直接通过类和点运算符实现,当然也可以使用KVC。不过对于私有属性,点运算符就不起作用,因为私有属性不暴露给调用者,不过使用KVC却依然可以实现对私有属性的读写。

先看一下KVC的一部分源码,当然只能看到头文件:

// NSKeyValueCoding.h

@interface NSObject(NSKeyValueCoding)

+ (BOOL)accessInstanceVariablesDirectly;

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath NS_AVAILABLE(10_7, 5_0);
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

@end


@interface NSArray<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end


@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)

- (nullable ObjectType)valueForKey:(NSString *)key;

@end


@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)

- (void)setValue:(nullable ObjectType)value forKey:(NSString *)key;

@end


@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);
- (void)setValue:(nullable id)value forKey:(NSString *)key NS_AVAILABLE(10_7, 5_0);

@end


@interface NSSet<ObjectType>(NSKeyValueCoding)

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

@end

可以看到这个类里面包含了对类NSObject,NSArray,NSDictionary,NSMutableDictionary,NSOrderedSet,NSSet的拓展。拓展的方法基本上为

- (id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

也就是说,基本上Objective-C里所有的对象都支持KVC操作,操作包含如上两类方法,动态读取和动态设值。

好多地方说是NSObject实现了NSKeyValueCoding协议。而代码里是类的拓展。这两种说法是相通的嘛?

举个?,新建一个Command line程序:

// Account.h
@interface Account : NSObject

@property (nonatomic, assign) float balance;

@end

// Account.m
@implementation Account {
    float salaryPerDay;
}

@synthesize balance = _balance;

- (void)setBalance:(float)balance {
    NSLog(@"set balance invoked");
    _balance = balance;
}

- (float)balance {
    NSLog(@"get balance invoked");
    return _balance;
}

@end

// Person.h
@class Account;

@interface Person : NSObject {
    @private
    int _age;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, retain) Account *account;

- (void)showMessage;

@end

// Person.m
#import "Person.h"

@implementation Person {
    NSString *_sex;
}

- (void)showMessage {
    NSLog(@"name = %@, age = %d, sex = %@", _name, _age, _sex);
}

@end

// main
int main(int argc, const char * argv[]) {
    @autoreleasepool {
      
        Person *person1 = [[Person alloc] init];
        [person1 setValue:@"Wossoneri" forKey:@"name"];
        [person1 setValue:@25 forKey:@"age"];       //私有变量也可以访问
        [person1 setValue:@"male" forKey:@"sex"];   //私有变量也可以访问
        
        [person1 showMessage];
        
        Account *account1 = [[Account alloc] init];
        person1.account = account1;
        
        [person1 setValue:@1000.0 forKeyPath:@"account.balance"];
        [person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];

        NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
        NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);

    }
    return 0;
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
Person1`s balance is : 1000.00
Person1`s salary is : 300.00

代码说明:

  • Person类里用旧方法声明私有变量_age以及直接添加的私有成员变量_sex,同时声明一个开放的属性_name
  • 对于_name,O-C会直接为其生成对应的settergetter,所以可以通过点运算符操作属性,比如 person1.name = @"Wossoneri";
  • 可以看到KVC可以对私有变量进行操作。对于当前类的直接成员变量,把变量名作为key来访问,否则要写成keyPath来访问。
  • KVC运行时首先会优先调用属性的gettersetter,这一点可以在代码输出的第二行和第三行看到,如果没有,就会优先搜索_property,不存在则搜索property,如果仍然没有,就会调用setValue:forUndefinedKey:valueForUndefinedKey:方法

KVO Key Value Observing

介绍

KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。

更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知

<NSKeyValueObserving> 或者 KVO 是一个非正式的协议,该协议定义了一个观察和通知对象之间状态变化的通用机制。作为一个非正式的协议,你在使用该协议的类中看不到惯用的写法<NSKeyValueObserving>,实际上,这个协议只是隐式地由NSObject实现,继承NSObject的子类默认能够使用这个协议。

放一部分NSKeyValueObserving.h对于NSObject的拓展代码

@interface NSObject(NSKeyValueObserving)

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

@end

@interface NSObject(NSKeyValueObserverRegistration)

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context NS_AVAILABLE(10_7, 5_0);
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

从拓展名称就可以看出,使用KVO需要注册监听器,也需要删除监听器。监听过程需要使用observeValueForKeyPath回调方法。 所以使用方法就可以推测出个大概来:

  1. addObserver方法注册一个监听器
  2. 复写observeValueForKeyPath回调,获得监听到的信息,做对应操作。
  3. 使用结束removeObserver,这很重要。

实现

最后对上面代码做一些改动,我需要对Account对象的balance做监听,当balance内容改变,我要做输出处理。

#pragma mark - For KVO
- (void)setAccount:(Account *)account {
    _account = account;
    //add observer
    [_account addObserver:self
               forKeyPath:@"balance"
                  options:NSKeyValueObservingOptionNew
                  context:nil];
}

//override
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    
    if ([keyPath isEqualToString:@"balance"]) {
        NSLog(@"keyPath = %@, object = %@, newValue = %.2f, context = %@", keyPath, object, [[change objectForKey:@"new"] floatValue], context);
    }
}

- (void)dealloc {
    [_account removeObserver:self forKeyPath:@"balance"];
}

//main
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *person1 = [[Person alloc] init];
        [person1 setValue:@"Wossoneri" forKey:@"name"];
        [person1 setValue:@25 forKey:@"age"];       //私有变量也可以访问
        [person1 setValue:@"male" forKey:@"sex"];   //私有变量也可以访问

        [person1 showMessage];
        
        Account *account1 = [[Account alloc] init];
        person1.account = account1;
        
        [person1 setValue:@1000.0 forKeyPath:@"account.balance"];
        [person1 setValue:@300.0 forKeyPath:@"account.salaryPerDay"];
        
        
        //KVO
        account1.balance = 4000.0;

        NSLog(@"Person1`s balance is : %.2f", [[person1 valueForKeyPath:@"account.balance"] floatValue]);
        NSLog(@"Person1`s salary is : %.2f", [[person1 valueForKeyPath:@"account.salaryPerDay"] floatValue]);
}

// 输出
name = Wossoneri, age = 25, sex = male
set balance invoked
get balance invoked
keyPath = balance, object = <Account: 0x1003001d0>, newValue = 1000.00, context = (null)
set balance invoked
get balance invoked
keyPath = balance, object = <Account: 0x1003001d0>, newValue = 4000.00, context = (null)
get balance invoked
Person1`s balance is : 4000.00
Person1`s salary is : 300.00

优点

当有属性改变,KVO会提供自动的消息通知。这样的架构有很多好处。首先,开发人员不需要自己去实现这样的方案:每次属性改变了就发送消息通知。这是KVO机制提供的最大的优点。因为这个方案已经被明确定义,获得框架级支持,可以方便地采用。开发人员不需要添加任何代码,不需要设计自己的观察者模型,直接可以在工程里使用。其次,KVO的架构非常的强大,可以很容易的支持多个观察者观察同一个属性,以及相关的值。

Swift的KVO与KVC

Swift版本的的就看这篇文章吧,内容很详细。 漫谈 KVC 与 KVO

Reference iOS开发系列—Objective-C之KVC、KVO

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术总结

DWIntrosPage 简单定制引导页

下面摘取部分代码 DWIntrosPageContentViewController

1345
来自专栏進无尽的文章

编码篇-持久化NSUserDefaults

NSUserDefaults类,以字典形式保存数据,IOS会自动把字典中的键值对转换成对应的XML文件(也就是plist文件),这个文件会被保存到APP的沙盒目...

1241
来自专栏向治洪

RCTEventEmitter使用

在0.27版本之前,RN的Native端向js端发射消息主要通过sendDeviceEventWithName的方式,相关代码如下。 @synthesize b...

3827
来自专栏陈满iOS

iOS网络请求之上传图片:从示例到源码解析 -- 以上传Face++SDK回调的图片为例(HYNetworking,AFNetworking,XMNetworking)

本文一开始上传图片以调用HYNetworking的API为例,这个网络框架是以AFNetworking为基础进行的封装。HYNetworking内部实现上传图片...

2312
来自专栏流柯技术学院

使用loadrunner进行压力测试之----post请求

2. 如果要发送的请求的数据值需要变化,那么需要将请求中的值参数化,,如果是根据上一条请求的返回值来确定请求中的数据值,那么需要对上一条请求的返回值进行解析

981
来自专栏.NET开发那点事

关于Form.Close跟Form.Dispose

我们在Winform开发的时候,使用From.Show来显示窗口,使用Form.Close来关闭窗口。熟悉Winform开发的想必对这些非常熟悉。但是Form类...

2206
来自专栏Alice

iOS 获取通讯录里边的电话号码AddressBook

1  首先导入库 <AddressBook/AddressBook.h> 2 然后在导入#import <AddressBook/AddressBook.h>文...

28610
来自专栏雨尘分享

ReactiveCocoa 入门知识——归总篇

2214
来自专栏王大锤

iOS 根据生日得到生肖,星座,年龄的算法

4886
来自专栏一“技”之长

iOS通过NSUserDefaults实现简单的应用间数据传递

NSUserDefaults是用于保存应用程序设置,应用信息等轻量级数据的的一个类,其本质是将数据写为plist文件的形式保存在本地。在IOS中,系统为每一个应...

932

扫码关注云+社区

领取腾讯云代金券