轻松掌握 Cocoa 框架下的 Key-Value 编程:KVC 和 KVO

KVC 探索

KVC 是 Key-Value Coding 的缩写,它是苹果 macOS 和 iOS 中 Cocoa 和 Cocoa Touch 框架中使用的一种机制。KVC 允许间接访问对象的属性,使用字符串来标识属性名称,而不是直接调用方法或访问实例变量。这提供了面向对象编程中更多的灵活性和抽象性。

KVC 通常与其他的 Cocoa 和 Cocoa Touch 框架一起使用,例如 Key-Value Observing 绑定,以提供强大和灵活的用户界面和数据模型。

KVC 设值原理

// Person.h @interface Person : NSObject 
@property (nonatomic, copy) NSString *name; 
@property (nonatomic, copy) NSString *gender; 
@end 
// Person.m 
@interface Person () 
@property (nonatomic, assign) int age; 
@end 
// 调用 
Person *p = [[Person alloc] initWithName:@"小王" gender:@"男" age:18]; 
[p print]; 
[p setValue:@28 forKey:@"age"]; 
// KVC 设值 
[p print]; 
// 打印 
姓名 = 小王, 性别 = 男,私密年龄 = 18 
姓名 = 小王, 性别 = 男,私密年龄 = 28

age 属性是私有变量,在这里我们通过 KVC 可以访问到 Person 的私有变量,那么,KVC 是怎么帮助我们通过字符串对属性进行访问的呢?如下图所示:

iOS-KVC (3).jpg

  • 第一步,KVC 首先会按顺序查找对象是否有名为 set,_set 的方法,其中 是属性名,在例子中表示 setAge,_setAge 方法。如果找到了,它会将属性值作为参数传递给这个方法,完成修改;如果没有找到,则进入第二步。
  • 第二步,KVC 会先访问 acessInstanceVariablesDirectly 方法询问是否有访问私有变量的权限,如果不允许,则直接进入第三步;如果允许,则按照 _、_is、、is 顺序依次查找是否有成员变量,如果有,则直接修改成员变量;如果没有,则进入第三步。
  • 第三步,对象既没有类似的方法实例变量,KVC 会直接调用 setValue:forUndefinedKey: 方法,这个方法同时会抛出一个 NSUndefinedKeyException 异常。

因此,使用KVC设置属性值时,属性名称必须与对象实际拥有的属性名称完全匹配,否则就会抛出NSUnknownKeyException异常。此外,KVC 只能用于设置对象属性,不能用于设置结构体属性或 C 数组属性,对于值类型或结构体类型,需要手动封装成 NSNumber 和 NSValue 再传入。

KVC 取值原理

KVC 同样也提供取值方法,如下代码所示:

Person *p = [[Person alloc] initWithName:@"小王" gender:@"男" age:18];
NSLog([NSString stringWithFormat:@"name = %@, age = %@", p.name, [p valueForKey:@"age"]]);

为了实现取值,KVC 同样也设计了一套类似于设值的原理,如下图所示:

iOS-KVC 取值.jpg

  • 第一步,KVC 首先按照 get、、is 的顺序查找 getter 方法,找到调用后返回 OC 对象;如果没找到,则进入第二步。
  • 第二步,KVC 会先访问 acessInstanceVariablesDirectly 方法询问是否有访问私有变量的权限,如果不允许,则直接进入第三步;如果允许,则按照 _、_is、、is 顺序依次查找是否有成员变量,如果有,则返回实例变量的 OC 对象;如果没有,则进入第三步。
  • 第三步,如果对象既没有对应的实例变量,也没有对应的方法,KVC会调用对象的 valueForUndefinedKey: 方法,并将属性名称作为参数传递给它。这个方法会抛出一个 NSUndefinedKeyException 异常。

KVC 取值有一点需要我们注意,若实例变量不是 OC 对象,KVC 会自动帮我们转换,把值类型封装成 NSNumber,把结构体封装成 NSValue。

keyPath 使用

在实际开发中,我们会需要访问属性的属性,例如访问一个 Person 对象嵌套的 Address 对象的 city 属性。在 KVC 中,keyPath 是一个由属性名和点号(.)连接起来的字符串,用于指定一个对象的属性路径。它可以用于访问对象属性的值,也可以用于对集合属性进行过滤、映射、排序等操作。

通过 keyPath,我们可以访问对象的嵌套属性,可以使用 @”address.city” 访问到 被嵌套的 Address 对象的 city 属性。keyPath 也支持一些集合操作,提供了@”avg”、@”count”、@”max”、@”min”、@”sum” 等操作。比如,在一个包含 Book 对象的数组中计算所有书的总价、均价、数量、最小价格和最大价格,如下代码所示:

Book *book1 = [Book new];
book1.name = @"The Great Gastby"; 
book1.price = 10; 
Book *book2 = [Book new]; 
book2.name = @"Time History"; 
book2.price = 20; 
Book *book3 = [Book new]; 
book3.name = @"Wrong Hole"; 
book3.price = 30; 
Book *book4 = [Book new]; 
book4.name = @"Wrong Hole"; 
book4.price = 40; 
NSArray *arrBooks = @[book1,book2,book3,book4]; 
NSNumber *sum = [arrBooks valueForKeyPath:@"@sum.price"]; 
NSLog(@"sum:%f",sum.floatValue); 
NSNumber *avg = [arrBooks valueForKeyPath:@"@avg.price"]; 
NSLog(@"avg:%f",avg.floatValue); 
NSNumber *count = [arrBooks valueForKeyPath:@"@count"]; 
NSLog(@"count:%f",count.floatValue); 
NSNumber *min = [arrBooks valueForKeyPath:@"@min.price"]; 
NSLog(@"min:%f",min.floatValue); 
NSNumber *max = [arrBooks valueForKeyPath:@"@max.price"]; 
NSLog(@"max:%f",max.floatValue);

// 输出 
sum:100.000000 
avg:25.000000 
count:4.000000 
min:10.000000 
max:40.000000

小结

我们通过对 KVC 的学习和理解,可以发现 KVC 是一种非常有用的技术,以至于在 Swift 仍保留这个机制。合理使用 KVC,可以给我们带来以下便利:

  • KVC 能够动态的对 Objective-C 对象的实例(包括私有实例)进行设值和取值;
  • KVC 对 Objective-C 容器 setValue:forKey: 和 valueForKey: 做了特殊优化,如 NSMutableDictionary 对象调用 setValue:forKey: 进行设值,传入的 value 为 nil 时,NSMutableDictionary 对象会自动调用 removeObject:forKey:。

KVO 探索

KVO (Key-Value Observing) 是指通过观察者模式来监听对象属性的变化。当被观察对象的属性发生变化时,KVO 会自动通知注册的观察者对象,观察者对象可以在接收到通知后执行相应的操作。

在 Objective-C 和 Swift 中,KVO 是一种非常常用的技术,可以帮助开发者编写更加灵活、可扩展的代码。

KVO 使用

KVO 的使用并不复杂,例如我们要对一个 Person 对象的 name 属性进行观察。

第一步,先给 Person 对象注册观察者对象:在 Person 对象上调用 addObserver:forKeyPath:options:context: 方法,注册观察者对象。该方法会将观察者对象添加到 Person 对象的观察者列表中,并指定要观察的属性和观察选项。如下代码所示:

// 创建一个被观察对象 
Person *person = [[Person alloc] init]; 
// 注册观察者对象 
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
  • observer 是观察者对象,这里指的是当前实例;
  • keyPath 指的是 Person 对象中被观察的 name 属性,允许嵌套;
  • options 是 NSKeyValueObservingOptions 类型的枚举,通过 options 可以定义观察行为;
  • context 表示上下文,用于区分消息,一般传入 nil。

NSKeyValueObservingOptions 类型的枚举有四类,其中 NSKeyValueObservingOptionNew 表示 Person 对象的 name 属性修改后的新值;NSKeyValueObservingOptionOld 表示 Person 对象中 name 属性修改前的旧值;NSKeyValueObservingOptionInitial 表示 Person 对象注册了观察者后会立马调用观察者方法;NSKeyValueObservingOptionPrior 表示 Person 对象的 name 属性被修改前也会调用一次观察者方法,因此修改一次 name 属性,会调用两次观察者方法。

第二步,实现观察者方法:在观察者对象中实现 observeValueForKeyPath:ofObject:change:context: 方法,该方法会在 Person 对象的属性变化时被调用,可以在该方法中实现相应的操作。如下代码所示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { 
    if ([keyPath isEqualToString:@"name"]) { 
        NSLog(@"Name changed to %@", change[NSKeyValueChangeNewKey]); 
    }
}
  • keyPath 是 Person 对象中被观察的属性名称,和注册时传入的 keyPath 一样是;
  • object 表示观察对象;
  • change 是一个字典,用于获取 Person 对象属性变化前后的值都;
  • context 是注册时传入的 context,用于区分不同的消息。

第三步,修改 Person 对象的 name 属性时,需要用 KVC 或属性调用,才能触发 KVO 的机制,如下代码所示:

// 修改被观察对象的属性 
person.name = @"Tom"; 
[person setValue:@"Tom" forKey:@"name"];

第四步,在不需要再监听属性变化时,需要调用 removeObserver:forKeyPath: 方法,将观察者对象从被观察对象的观察者列表中移除。如下代码所示:

[person removeObserver:self forKeyPath:@"name"];

KVO 原理

KVO 的原理是基于 Objective-C 的 Runtime 机制实现的,当我们为一个对象注册观察者时,KVO 会在运行时动态生成一个新类,并将原对象的 isa 指针指向新类,从而实现对原对象的属性观察。如下图所示:

iOS-KVO 原理 (1).jpg

  • KVO 会在运行时动态生成个名为 NSKVONotifying_Person 的类,该类继承自 Person 类,并重写了 setValue:forKey: 和 valueForKey: 方法;
  • 当我们为 Person 对象的 name 属性注册观察者后,KVO 会将 name 属性的 setter 方法替换成新的 setter,从而实现属性变化时通知;
  • 当我们为 Person 对象注册观察者后,KVO 会将该对象的 isa 指针指向 NSKVONotifying_Person 类,从而使得该对象的所有方法的调用都会被转发到 NSKVONotifying_Person 类中。所以当我们调用 Person 对象 name 属性的 setter 方法,实际上调用的是 NSKVONotifying_Person 类的 setter 方法,该方法会先调用父类的 setter 方法,然后再发送通知,通知所有观察者属性发生了变化。

有一点我们需要注意,KVO 只能观察对象的属性,不能观察成员变量,因为 KVO 是基于 setter 方法实现的,而成员变量的赋值不会触发 setter 方法的调用,所以 KVO 无法观察到成员变量的变化。

总结

今天我们分享了 KVC 和 KVO,我们显示介绍了相关的基础用法,也深入了解它们的本质,虽然我们平时可能用的不多,但两者结合起来的功能十分强大,我们可以借助他们来实现对象间的解耦和模型——视图的 Data Binding,从而提高代码的可拓展性和维护性。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYeeRwwx' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片