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

深入解析Objective-C中的KVC与KVO在运行时的工作机制

2022-02-251.3k 阅读

一、Objective-C 中的 KVC(Key - Value Coding)工作机制

1.1 KVC 概述

Key - Value Coding(KVC)是一种通过键值对来访问对象属性的机制,它为 Objective - C 提供了一种间接访问对象属性的方式。这种机制使得开发者可以通过一个键(通常是属性名的字符串表示)来访问对象的属性,而不需要直接通过对象的存取方法。KVC 在很多场景下都非常有用,比如在数据绑定、集合操作等方面。

1.2 KVC 的基础访问方法

在 Objective - C 中,使用 valueForKey: 方法来获取对象属性的值,使用 setValue:forKey: 方法来设置对象属性的值。例如,假设有一个简单的 Person 类:

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

@implementation Person
@end

我们可以这样使用 KVC 来访问和设置属性:

Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"name"];
NSString *name = [person valueForKey:@"name"];
NSLog(@"Name: %@", name);

当调用 valueForKey: 时,KVC 会按照一定的搜索路径来查找对应的属性值。首先,它会查找对象是否有一个与键名相同的 get<Key> 形式的方法,如果有则调用该方法获取值。如果没有,它会查找 is<Key> 形式的方法(适用于布尔类型属性)。如果还是没有找到,它会查找 _<key> 形式的实例变量,如果找到则直接返回该实例变量的值。如果仍然没有找到,它会查找 <key> 形式的实例变量。

对于 setValue:forKey: 方法,KVC 会先查找 set<Key>: 形式的方法,如果找到则调用该方法设置值。如果没有找到,它会查找 _<key> 形式的实例变量,如果找到则直接设置该实例变量的值。如果还是没有找到,它会查找 <key> 形式的实例变量并设置值。

1.3 KVC 的集合操作

KVC 不仅可以用于单个对象的属性访问,还可以对集合对象(如 NSArrayNSSet)进行操作。例如,假设有一个 Person 对象的数组:

Person *person1 = [[Person alloc] init];
person1.name = @"Alice";
person1.age = 25;
Person *person2 = [[Person alloc] init];
person2.name = @"Bob";
person2.age = 30;
NSArray *people = @[person1, person2];

我们可以通过 KVC 获取所有人的名字组成的数组:

NSArray *names = [people valueForKey:@"name"];
NSLog(@"Names: %@", names);

KVC 还支持一些集合操作符,比如 @avg 用于计算平均值,@sum 用于计算总和等。例如,计算所有人的平均年龄:

NSNumber *averageAge = [people valueForKeyPath:@"@avg.age"];
NSLog(@"Average Age: %@", averageAge);

1.4 KVC 在运行时的本质

在运行时,KVC 依赖于 Objective - C 的动态方法解析和消息转发机制。当调用 valueForKey:setValue:forKey: 且对象没有对应的直接方法或实例变量时,会触发动态方法解析。首先,类的 + (BOOL)resolveInstanceMethod:(SEL)sel 方法会被调用,开发者可以在这个方法中动态添加方法实现。如果动态方法解析没有成功,消息会被转发到 forwardingTargetForSelector:(SEL)aSelector 方法,这里可以返回一个能处理该消息的对象。如果这也没有成功,最终会调用 methodSignatureForSelector:(SEL)aSelectorforwardInvocation:(NSInvocation *)anInvocation 方法来进行完整的消息转发处理。

例如,假设 Person 类没有 height 属性,但我们想通过 KVC 来处理它:

@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(height)) {
        class_addMethod(self, sel, (IMP)customHeightGetter, "@@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

float customHeightGetter(id self, SEL _cmd) {
    return 175.0;
}
@end

然后我们就可以这样使用 KVC:

Person *person = [[Person alloc] init];
NSNumber *height = [person valueForKey:@"height"];
NSLog(@"Height: %@", height);

二、Objective - C 中的 KVO(Key - Value Observing)工作机制

2.1 KVO 概述

Key - Value Observing(KVO)是一种基于观察者模式的机制,它允许对象监听另一个对象特定属性的变化。当被观察对象的属性值发生改变时,观察者对象会收到通知,从而可以执行相应的操作。KVO 在很多场景下都有应用,比如数据模型与视图之间的同步,当数据模型中的属性变化时,视图可以自动更新。

2.2 KVO 的基本使用

在 Objective - C 中,使用 KVO 需要以下几个步骤:

  1. 注册观察者:通过 addObserver:forKeyPath:options:context: 方法为被观察对象的特定属性注册观察者。
  2. 实现观察方法:观察者需要实现 observeValueForKeyPath:ofObject:change:context: 方法,当被观察属性值变化时,该方法会被调用。
  3. 移除观察者:在适当的时候,通过 removeObserver:forKeyPath: 方法移除观察者,以避免内存泄漏。

例如,还是以 Person 类为例:

@interface Observer : NSObject
@end

@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"age"]) {
        NSLog(@"The age of the person has changed. New value: %@", change[NSKeyValueChangeNewKey]);
    }
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Observer *observer = [[Observer alloc] init];
        [person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        person.age = 26;
        [person removeObserver:observer forKeyPath:@"age"];
    }
    return 0;
}

2.3 KVO 的工作原理

KVO 是基于运行时机制实现的。当为一个对象的属性注册观察者时,系统会动态创建一个被观察对象的子类(通常是在运行时生成的),这个子类重写了被观察属性的 setter 方法。在 setter 方法中,会调用 willChangeValueForKey:didChangeValueForKey: 方法,这两个方法会通知观察者属性值即将改变和已经改变。

例如,假设 Person 类有一个 age 属性,当注册 KVO 后,系统生成的子类(假设为 NSKVONotifying_Person)的 setAge: 方法可能会类似这样实现:

- (void)setAge:(NSInteger)age {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

willChangeValueForKey: 方法会记录属性值即将改变的信息,而 didChangeValueForKey: 方法会触发观察者的 observeValueForKeyPath:ofObject:change:context: 方法,将属性值变化的信息传递给观察者。

2.4 KVO 的注意事项

  1. 内存管理:一定要记得在适当的时候移除观察者,否则可能会导致内存泄漏。例如,如果观察者对象在被观察对象之前释放,而没有移除观察者,当被观察对象属性变化时,会向已经释放的观察者对象发送消息,导致程序崩溃。
  2. 多线程问题:如果在多线程环境下使用 KVO,需要注意线程安全。因为 KVO 的通知是在属性值变化时异步发送的,如果多个线程同时修改被观察属性,可能会导致通知的顺序和预期不一致,甚至出现数据竞争问题。可以通过加锁等方式来确保线程安全。

三、KVC 与 KVO 的结合使用

在实际开发中,KVC 和 KVO 经常结合使用。例如,在一个复杂的数据模型中,可能有多层嵌套的对象结构,通过 KVC 可以方便地访问深层嵌套对象的属性,而 KVO 可以监听这些属性的变化。

假设有一个 Company 类,它包含一个 Department 数组,每个 Department 又包含一个 Employee 数组:

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

@implementation Employee
@end

@interface Department : NSObject
@property (nonatomic, copy) NSString *departmentName;
@property (nonatomic, strong) NSArray<Employee *> *employees;
@end

@implementation Department
@end

@interface Company : NSObject
@property (nonatomic, copy) NSString *companyName;
@property (nonatomic, strong) NSArray<Department *> *departments;
@end

@implementation Company
@end

我们可以通过 KVC 来获取公司某个部门某个员工的工资:

Company *company = [[Company alloc] init];
// 初始化公司、部门和员工数据
NSNumber *salary = [company valueForKeyPath:@"departments[0].employees[0].salary"];

同时,我们可以使用 KVO 来监听这个员工工资的变化:

Observer *observer = [[Observer alloc] init];
[company addObserver:observer forKeyPath:@"departments[0].employees[0].salary" options:NSKeyValueObservingOptionNew context:nil];
// 假设通过某种方式改变了员工工资
[company setValue:@(5000) forKeyPath:@"departments[0].employees[0].salary"];
[company removeObserver:observer forKeyPath:@"departments[0].employees[0].salary"];

在这个例子中,KVC 提供了一种简洁的方式来访问深层嵌套对象的属性,而 KVO 则可以对这些属性的变化进行监听,两者结合大大提高了代码的灵活性和可维护性。

四、KVC 和 KVO 的性能考量

4.1 KVC 的性能

KVC 的性能在不同情况下有所不同。当通过 KVC 访问简单对象的直接属性时,由于它需要经过一系列的查找过程(如查找存取方法、实例变量等),性能会略低于直接通过存取方法访问属性。但是,在处理集合对象和复杂的键路径时,KVC 的便利性带来的收益往往大于其性能损耗。

例如,在对一个包含大量对象的数组进行属性批量获取时,使用 KVC 的 valueForKey: 方法可以一行代码实现,而如果使用传统的遍历方式则需要更多的代码。在这种情况下,虽然 KVC 内部查找机制会消耗一些性能,但代码的简洁性和开发效率的提升更为重要。

然而,如果在性能敏感的代码段,如频繁调用的循环中使用 KVC 来访问简单属性,可能会对性能产生一定影响。此时,直接使用存取方法可能是更好的选择。

4.2 KVO 的性能

KVO 的性能主要受以下几个因素影响:

  1. 观察者数量:注册的观察者越多,每次属性变化时通知的开销就越大。因为系统需要遍历所有的观察者并调用它们的 observeValueForKeyPath:ofObject:change:context: 方法。
  2. 通知内容:如果在 observeValueForKeyPath:ofObject:change:context: 方法中执行复杂的操作,如大量的计算、文件读写等,会显著影响性能。在观察者方法中应尽量执行轻量级的操作,如更新视图等简单任务。
  3. 属性变化频率:如果被观察属性频繁变化,会导致大量的通知发送,增加系统开销。在这种情况下,可以考虑适当减少不必要的属性变化,或者在观察者方法中进行合并处理,减少实际执行的操作次数。

为了优化 KVO 的性能,可以在适当的时候减少观察者数量,合理设计观察者方法中的操作,以及控制被观察属性的变化频率。

五、KVC 和 KVO 在框架中的应用

5.1 KVC 和 KVO 在 Cocoa 框架中的应用

在 Cocoa 框架中,KVC 和 KVO 被广泛应用。例如,在 UITableViewUICollectionView 等视图与数据模型的绑定中,经常使用 KVC 来获取数据模型中的属性值并显示在视图上。而 KVO 则用于监听数据模型的变化,当数据模型中的数据发生改变时,自动更新视图。

UITableView 为例,假设我们有一个包含 Person 对象的数组作为数据源,通过 KVC 可以轻松地将 Personname 属性显示在 UITableViewCell 中:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    Person *person = self.people[indexPath.row];
    cell.textLabel.text = [person valueForKey:@"name"];
    return cell;
}

如果我们希望在 Personage 属性变化时,自动更新 UITableViewCell 中显示的年龄信息,可以使用 KVO 来监听 age 属性的变化,并在 observeValueForKeyPath:ofObject:change:context: 方法中更新对应的 UITableViewCell

5.2 KVC 和 KVO 在其他第三方框架中的应用

许多第三方框架也大量使用 KVC 和 KVO 来实现数据绑定、事件监听等功能。例如,ReactiveCocoa 框架就基于 KVO 进行了更高级的封装,提供了一种响应式编程的方式。在 ReactiveCocoa 中,可以通过对对象的属性创建信号(Signal),当属性值变化时,信号会发出新的值,开发者可以订阅这个信号并执行相应的操作。

另外,在一些数据持久化框架中,KVC 用于将对象的属性值与数据库中的字段进行映射,而 KVO 可以监听对象属性的变化,及时将变化同步到数据库中。

通过在各种框架中的应用,KVC 和 KVO 展现了其强大的功能和通用性,为开发者提供了便捷的开发方式。

六、总结 KVC 和 KVO 的运行时机制对开发的影响

  1. 代码的灵活性和可维护性:KVC 和 KVO 的运行时机制使得代码在访问对象属性和监听属性变化方面更加灵活。通过 KVC,可以使用字符串键来访问属性,这在一些动态配置和反射场景下非常有用。而 KVO 基于观察者模式的机制,使得代码的模块之间耦合度降低,数据模型和视图等模块可以相对独立地开发和维护。
  2. 调试的复杂性:由于 KVC 和 KVO 依赖于运行时的动态机制,如动态方法解析和消息转发,这增加了调试的难度。当出现问题时,很难直接从代码的表面看出问题所在。例如,在 KVC 中,如果键名拼写错误,可能不会在编译时报错,而是在运行时出现 NSUndefinedKeyException 异常,需要开发者仔细排查键名。在 KVO 中,如果没有正确移除观察者,可能会导致内存泄漏,但这种问题很难直接发现。
  3. 对内存管理的要求:KVO 中正确移除观察者对于内存管理至关重要。如果没有及时移除观察者,当被观察对象释放时,可能会导致野指针访问,引发程序崩溃。同时,KVC 在集合操作等场景下,如果使用不当,也可能会导致内存占用过高。
  4. 性能优化的挑战:如前面提到的,KVC 和 KVO 在性能方面都有一些需要注意的地方。开发者需要根据具体的应用场景,合理使用 KVC 和 KVO,以避免性能问题。例如,在性能敏感的代码段避免过度使用 KVC,合理控制 KVO 中的观察者数量和通知处理逻辑,以优化程序的性能。

总的来说,深入理解 KVC 和 KVO 的运行时机制,能够帮助开发者更好地利用这两个强大的特性,编写出更加灵活、健壮和高效的 Objective - C 代码。同时,也需要注意它们带来的调试、内存管理和性能等方面的挑战,通过合理的设计和编码来规避这些问题。