MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的KVC与KVO技术解析

2024-05-301.2k 阅读

一、KVC(Key - Value Coding)基础概念

KVC即键值编码,是一种间接访问对象属性的机制。通过KVC,开发者可以使用一个键(通常是字符串)来访问对象的属性,而不是通过直接调用存取方法。这种机制提供了一种统一的方式来访问对象属性,无论这些属性是简单类型还是复杂对象。

在Objective - C中,KVC基于NSKeyValueCoding协议实现。NSObject类遵循了该协议,这意味着几乎所有的Objective - C对象都可以使用KVC机制。

二、KVC的基本用法

(一)简单属性访问

假设我们有一个名为Person的类,具有name和age属性:

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

@implementation Person
@end

我们可以通过KVC来访问和设置这些属性:

Person *person = [[Person alloc] init];
// 使用KVC设置属性值
[person setValue:@"John" forKey:@"name"];
[person setValue:@25 forKey:@"age"];

// 使用KVC获取属性值
NSString *name = [person valueForKey:@"name"];
NSNumber *ageNumber = [person valueForKey:@"age"];
NSInteger age = [ageNumber integerValue];

在上述代码中,setValue:forKey:方法用于设置属性值,valueForKey:方法用于获取属性值。注意,对于基本数据类型,KVC会自动进行装箱和拆箱操作,例如将NSInteger包装成NSNumber

(二)嵌套属性访问

KVC支持对嵌套对象的属性进行访问。例如,假设我们有一个Company类,其中包含一个Person类型的属性ceo:

@interface Company : NSObject
@property (nonatomic, strong) Person *ceo;
@end

@implementation Company
@end

我们可以通过KVC来访问ceo的属性:

Company *company = [[Company alloc] init];
Person *ceo = [[Person alloc] init];
[ceo setValue:@"Alice" forKey:@"name"];
[company setValue:ceo forKey:@"ceo"];

// 访问嵌套属性
NSString *ceoName = [company valueForKeyPath:@"ceo.name"];

这里使用valueForKeyPath:方法,通过一个路径字符串来访问嵌套对象的属性。ceo.name表示先获取ceo属性,然后再获取ceo对象的name属性。

三、KVC的集合操作

(一)对集合对象的简单操作

当一个对象的属性是集合类型(如NSArray或NSSet)时,KVC提供了一些方便的集合操作方法。例如,假设我们有一个Department类,其中包含一个Person数组的属性employees:

@interface Department : NSObject
@property (nonatomic, strong) NSArray <Person *> *employees;
@end

@implementation Department
@end

我们可以通过KVC获取所有员工的名字:

Department *department = [[Department alloc] init];
Person *p1 = [[Person alloc] init];
[p1 setValue:@"Bob" forKey:@"name"];
Person *p2 = [[Person alloc] init];
[p2 setValue:@"Charlie" forKey:@"name"];
department.employees = @[p1, p2];

NSArray *names = [department valueForKeyPath:@"employees.name"];

上述代码通过employees.name路径,获取了所有员工的名字组成的数组。

(二)集合操作符

KVC还提供了一些集合操作符,用于对集合进行更复杂的操作,如求和、平均值、最大值、最小值等。

  1. 求和操作 假设Person类新增一个表示工资的属性salary:
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat salary;
@end

@implementation Person
@end

我们可以计算部门所有员工的工资总和:

Department *department = [[Department alloc] init];
Person *p1 = [[Person alloc] init];
[p1 setValue:@5000.0 forKey:@"salary"];
Person *p2 = [[Person alloc] init];
[p2 setValue:@6000.0 forKey:@"salary"];
department.employees = @[p1, p2];

NSNumber *totalSalary = [department valueForKeyPath:@"employees.@sum.salary"];
CGFloat sum = [totalSalary floatValue];

这里@sum是集合操作符,表示对employees集合中每个对象的salary属性进行求和。

  1. 平均值操作 计算员工工资的平均值:
NSNumber *averageSalary = [department valueForKeyPath:@"employees.@avg.salary"];
CGFloat avg = [averageSalary floatValue];

@avg操作符用于计算平均值。

  1. 最大值和最小值操作 获取员工工资的最大值和最小值:
NSNumber *maxSalary = [department valueForKeyPath:@"employees.@max.salary"];
NSNumber *minSalary = [department valueForKeyPath:@"employees.@min.salary"];
CGFloat max = [maxSalary floatValue];
CGFloat min = [minSalary floatValue];

@max@min分别用于获取最大值和最小值。

四、KVC的实现原理

当调用valueForKey:方法时,KVC会按照以下顺序查找属性:

  1. 查找存取方法:首先,KVC会查找标准的存取方法,例如对于属性name,会查找name方法和setName:方法。如果找到了,就会调用相应的方法来获取或设置属性值。
  2. 查找实例变量:如果没有找到存取方法,KVC会尝试直接访问实例变量。它会查找名为_namename等的实例变量。
  3. 动态方法解析:如果前两步都失败,KVC会触发动态方法解析机制。类可以通过实现+ (BOOL)resolveInstanceMethod:(SEL)sel方法来动态添加方法。
  4. 备用的KVC方法:如果动态方法解析也失败,KVC会调用对象的- (id)forwardingTargetForSelector:(SEL)aSelector方法,看是否有其他对象可以处理该消息。
  5. 完整的消息转发:最后,如果以上步骤都失败,会进入完整的消息转发流程,通过- (void)forwardInvocation:(NSInvocation *)anInvocation方法来处理未识别的消息。

五、KVO(Key - Value Observing)基础概念

KVO即键值观察,是一种基于观察者模式的机制。它允许开发者监听对象属性值的变化。当被观察对象的某个属性值发生改变时,观察者会收到通知,从而可以做出相应的响应。

在Objective - C中,KVO基于NSKeyValueObserving协议实现。同样,NSObject类遵循了该协议,使得大多数对象都可以使用KVO机制。

六、KVO的基本用法

(一)注册观察者

假设我们还是以Person类为例,要观察其age属性的变化。首先需要注册观察者:

Person *person = [[Person alloc] init];
[self addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

在上述代码中,addObserver:forKeyPath:options:context:方法用于注册观察者。其中,self表示观察者本身(这里假设在一个视图控制器中),@"age"是要观察的属性的键路径,NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld表示在通知中会包含属性变化前后的新旧值,context是一个可选的上下文指针,用于在回调中区分不同的观察情况。

(二)实现观察回调方法

注册观察者后,需要实现observeValueForKeyPath:ofObject:change:context:方法来处理属性变化的通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSNumber *oldValue = change[NSKeyValueChangeOldKey];
        NSNumber *newValue = change[NSKeyValueChangeNewKey];
        NSLog(@"Age changed from %ld to %ld", (long)[oldValue integerValue], (long)[newValue integerValue]);
    }
}

在这个方法中,keyPath表示发生变化的属性的键路径,object表示被观察的对象,change是一个字典,包含了属性变化的相关信息,如新旧值,context就是注册观察者时传入的上下文指针。

(三)移除观察者

当不再需要观察某个属性时,需要移除观察者,以避免内存泄漏:

[self removeObserver:self forKeyPath:@"age"];

removeObserver:forKeyPath:方法用于移除观察者。

七、KVO的实现原理

KVO是通过运行时动态创建一个被观察对象的子类来实现的。当注册观察者时,系统会动态创建一个被观察对象类的子类,并重写被观察属性的存取方法。在重写的存取方法中,系统会在属性值变化时发送通知给观察者。

例如,对于Person类,当注册对其age属性的观察后,系统会创建一个类似于NSKVONotifying_Person的子类。这个子类会重写setAge:方法,在设置新值前后发送通知给观察者。

八、KVO的注意事项

  1. 内存管理:一定要记得在适当的时候移除观察者,否则可能会导致内存泄漏。特别是当被观察对象的生命周期比观察者短,而观察者持有对被观察对象的强引用时,不移除观察者会导致被观察对象无法释放。
  2. 线程安全:KVO通知默认是在主线程发送的,但如果被观察对象的属性在其他线程中被修改,可能会导致一些线程安全问题。在多线程环境下使用KVO时,需要注意同步和线程安全。
  3. 观察嵌套属性:观察嵌套属性时,需要注意键路径的正确性。同时,对于集合中的对象属性变化的观察,可能需要使用更复杂的机制,如集合代理或手动管理观察。

九、KVC与KVO的结合使用

在实际开发中,KVC和KVO常常结合使用。例如,在一个数据模型中,我们可以使用KVC来访问和修改复杂对象的属性,同时使用KVO来监听这些属性的变化,以便及时更新界面或进行其他业务逻辑处理。

假设我们有一个复杂的数据模型,其中包含多个嵌套的对象和集合。通过KVC可以方便地获取和设置深层嵌套的属性值,而KVO可以在这些属性值变化时通知相关的视图或业务逻辑模块。

// 使用KVC设置嵌套属性值
[company setValue:@30 forKeyPath:@"ceo.age"];

// 使用KVO监听嵌套属性变化
[self addObserver:self forKeyPath:@"ceo.age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

这样,当ceoage属性发生变化时,观察者会收到通知,从而可以做出相应的处理,比如更新显示ceo年龄的UI。

十、KVC与KVO在iOS开发框架中的应用

  1. UIKit框架:在UIKit中,虽然没有直接公开使用KVC和KVO的大量示例,但一些底层机制可能依赖于这些技术。例如,当数据模型发生变化时,视图的更新可能间接通过KVO机制实现。开发者也可以手动使用KVC和KVO来实现自定义视图与数据模型之间的绑定。
  2. Core Data框架:Core Data框架大量使用了KVC和KVO。Core Data的实体属性访问可以通过KVC进行,而实体属性的变化通知则依赖于KVO。这使得开发者可以方便地监听数据模型的变化,并及时更新UI或进行其他数据处理。
  3. MVVM架构:在MVVM(Model - View - ViewModel)架构中,KVC和KVO发挥着重要作用。ViewModel通过KVO监听Model的变化,并将相关数据通过KVC提供给View。View也可以通过KVC与ViewModel进行交互,这种机制实现了数据的双向绑定,提高了代码的可维护性和可测试性。

十一、总结KVC与KVO的优势与局限

  1. KVC的优势
    • 灵活性:提供了一种统一且灵活的方式来访问和修改对象属性,无需直接调用存取方法,对于动态获取和设置属性非常方便。
    • 集合操作:强大的集合操作功能,使得对集合对象的处理变得简单高效,如求和、平均值等操作一行代码即可实现。
    • 与其他框架的兼容性:在很多iOS框架(如Core Data)中都有广泛应用,便于与其他技术集成。
  2. KVC的局限
    • 性能问题:相比直接调用存取方法,KVC的查找和访问机制相对复杂,可能会带来一定的性能开销,尤其是在频繁访问属性的场景下。
    • 可读性:对于不熟悉KVC的开发者,使用KVC的代码可能较难理解,特别是在处理复杂的键路径时。
  3. KVO的优势
    • 观察者模式的实现:基于观察者模式,实现了对象间的解耦,使得被观察对象和观察者之间的依赖关系更加松散,便于代码的维护和扩展。
    • 数据变化监听:方便地监听对象属性的变化,这在数据模型与视图的绑定、数据同步等场景中非常有用。
  4. KVO的局限
    • 内存管理要求高:需要严格管理观察者的注册和移除,否则容易导致内存泄漏。
    • 线程安全问题:在多线程环境下需要额外处理线程安全问题,增加了开发的复杂性。

通过深入理解KVC和KVO的原理、用法及注意事项,开发者可以在Objective - C项目中更加高效地开发出健壮、可维护的代码,充分利用这两种强大的技术来提升应用的质量和性能。无论是处理复杂的数据模型,还是实现数据与界面的动态绑定,KVC和KVO都提供了非常有效的解决方案。在实际开发中,应根据具体的业务需求和场景,合理地运用KVC和KVO,以达到最佳的开发效果。同时,随着iOS开发技术的不断发展,KVC和KVO也在不断演进和完善,开发者需要持续关注其最新特性和应用方式,以保持技术的先进性。