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

Objective-C 中的 KVO(键值观察)原理与应用

2022-11-142.3k 阅读

1. KVO 基础概念

1.1 什么是 KVO

在 Objective-C 编程中,KVO(Key - Value Observing,键值观察)是一种观察者设计模式的实现。它允许对象监听另一个对象属性值的变化。当被观察对象的特定属性发生改变时,观察者对象会收到通知,从而可以做出相应的响应。

KVO 提供了一种简洁的机制来实现对象间的低耦合通信。传统的对象间通信方式,比如委托(delegate)模式,需要被观察对象明确地调用委托方法来通知委托对象。而 KVO 则通过一种更通用、更灵活的方式,让对象可以在不改变被观察对象代码的情况下,对其属性变化进行监听。

1.2 KVO 的应用场景

  • 界面更新:当模型数据发生变化时,通过 KVO 可以自动通知视图更新显示。例如,在一个待办事项应用中,当待办事项的完成状态属性发生改变时,通过 KVO 可以通知视图刷新,以显示正确的完成标记。
  • 数据同步:在多线程环境或不同模块间进行数据同步时,KVO 可以方便地监听数据变化,从而进行相应的同步操作。比如,在一个跨平台应用中,不同平台的模块可能需要同步某些核心数据,KVO 可以帮助实现这一功能。
  • 状态监控:对于一些对象状态的监控,KVO 提供了一种简单有效的方式。例如,监控网络连接状态对象的属性变化,当网络连接状态改变时,进行相应的处理,如重新加载数据等。

2. KVO 原理剖析

2.1 动态创建子类

在使用 KVO 时,系统会在运行时动态为被观察对象创建一个子类。这个子类继承自被观察对象的类,并在运行时被插入到类继承体系中。

以一个简单的 Person 类为例:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

当我们对 Person 对象的 name 属性进行 KVO 观察时,系统会动态创建一个类似于 NSKVONotifying_Person 的子类。这个子类继承自 Person 类。

2.2 重写属性的 setter 方法

动态创建的子类会重写被观察属性的 setter 方法。在重写的 setter 方法中,除了设置属性值之外,还会通知所有注册的观察者属性值已经发生了变化。

假设我们观察 Person 类的 name 属性,动态子类 NSKVONotifying_PersonsetName: 方法可能如下:

@implementation NSKVONotifying_Person
- (void)setName:(NSString *)name {
    // 通知观察者属性即将改变
    [self willChangeValueForKey:@"name"];
    // 调用父类的 setter 方法设置属性值
    [super setName:name];
    // 通知观察者属性已经改变
    [self didChangeValueForKey:@"name"];
}
@end

willChangeValueForKey:didChangeValueForKey: 这两个方法是 KVO 机制的重要组成部分。willChangeValueForKey: 方法在属性值即将改变时调用,而 didChangeValueForKey: 方法在属性值已经改变后调用。

2.3 观察者注册与通知发送

当一个对象注册为另一个对象属性的观察者时,会在运行时将自身添加到被观察对象的观察者列表中。当被观察对象属性值发生改变,调用 didChangeValueForKey: 方法时,会遍历观察者列表,向每个观察者发送通知。

观察者通过实现 observeValueForKeyPath:ofObject:change:context: 方法来接收通知。在这个方法中,观察者可以根据具体的变化情况进行相应的处理。

3. KVO 使用步骤

3.1 注册观察者

在使用 KVO 之前,需要先注册观察者。这可以通过调用被观察对象的 addObserver:forKeyPath:options:context: 方法来完成。

// 创建被观察对象
Person *person = [[Person alloc] init];
// 创建观察者对象
MyObserver *observer = [[MyObserver alloc] init];
// 注册观察者
[person addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

addObserver:forKeyPath:options:context: 方法中:

  • observer 是观察者对象,即接收通知的对象。
  • forKeyPath 是要观察的属性的键路径。对于简单属性,键路径就是属性名;对于嵌套属性,键路径是由点分隔的属性名序列。
  • options 是一个掩码值,用于指定通知的选项。例如,NSKeyValueObservingOptionNew 表示通知中包含新的属性值,NSKeyValueObservingOptionOld 表示通知中包含旧的属性值。
  • context 是一个指针,通常用于传递一些上下文信息给观察者。如果不需要传递额外信息,可以设置为 nil

3.2 实现观察方法

观察者需要实现 observeValueForKeyPath:ofObject:change:context: 方法来处理接收到的通知。

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSString *newName = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's name has changed to: %@", newName);
    }
}
@end

observeValueForKeyPath:ofObject:change:context: 方法中:

  • keyPath 是发生变化的属性的键路径。
  • object 是被观察的对象。
  • change 是一个字典,包含了属性变化的相关信息,如新旧值等。根据注册时设置的 options,字典中会包含相应的键值对。
  • context 是注册观察者时传递的上下文指针。

3.3 移除观察者

当不再需要观察某个对象的属性变化时,需要移除观察者。这可以通过调用被观察对象的 removeObserver:forKeyPath: 方法来完成。

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

移除观察者是很重要的,否则可能会导致内存泄漏或其他未定义行为。特别是当被观察对象的生命周期短于观察者对象时,如果不移除观察者,当被观察对象被释放后,观察者可能会收到野指针引用,从而导致程序崩溃。

4. KVO 高级应用

4.1 观察集合属性

KVO 不仅可以观察普通属性,还可以观察集合属性,如 NSArrayNSSet 等。当集合中的元素发生添加、删除或替换等操作时,观察者也会收到通知。

以观察 NSArray 属性为例,假设我们有一个 Company 类,包含一个 employees 属性,类型为 NSArray

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

@implementation Company
@end

要观察 employees 集合的变化,需要使用 NSKeyValueObservingOptionInitialNSKeyValueObservingOptionPrior 选项。

Company *company = [[Company alloc] init];
MyObserver *observer = [[MyObserver alloc] init];
[company addObserver:observer forKeyPath:@"employees" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:nil];

在观察者的 observeValueForKeyPath:ofObject:change:context: 方法中,可以根据变化字典中的信息来处理集合的变化。

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"employees"]) {
        NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue];
        switch (kind) {
            case NSKeyValueChangeInsertion: {
                NSArray *newEmployees = change[NSKeyValueChangeNewKey];
                NSLog(@"New employees added: %@", newEmployees);
                break;
            }
            case NSKeyValueChangeRemoval: {
                NSArray *removedEmployees = change[NSKeyValueChangeOldKey];
                NSLog(@"Employees removed: %@", removedEmployees);
                break;
            }
            case NSKeyValueChangeReplacement: {
                NSArray *oldEmployees = change[NSKeyValueChangeOldKey];
                NSArray *newEmployees = change[NSKeyValueChangeNewKey];
                NSLog(@"Employees replaced: old - %@, new - %@", oldEmployees, newEmployees);
                break;
            }
            default:
                break;
        }
    }
}
@end

4.2 使用上下文进行多观察区分

当一个观察者对象需要观察多个不同对象的多个属性时,可以通过上下文(context)来区分不同的观察。

假设 MyObserver 同时观察 Personname 属性和 age 属性。

static void *PersonNameContext = &PersonNameContext;
static void *PersonAgeContext = &PersonAgeContext;

Person *person = [[Person alloc] init];
MyObserver *observer = [[MyObserver alloc] init];
[person addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:PersonAgeContext];

observeValueForKeyPath:ofObject:change:context: 方法中,可以根据上下文进行区分处理。

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSString *newName = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's name has changed to: %@", newName);
    } else if (context == PersonAgeContext) {
        NSNumber *newAge = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's age has changed to: %@", newAge);
    }
}
@end

4.3 与其他设计模式结合

KVO 可以与其他设计模式结合使用,以实现更复杂的功能。例如,与 MVC(Model - View - Controller)模式结合,KVO 可以用于实现模型到视图的自动更新。当模型数据发生变化时,通过 KVO 通知视图进行更新,从而保持视图与模型的一致性。

在一个简单的 iOS 应用中,视图控制器(ViewController)可以作为观察者,观察模型对象的属性变化。当模型属性变化时,视图控制器可以更新视图。

@interface ViewController : UIViewController
@property (nonatomic, strong) Person *person;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[Person alloc] init];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSString *newName = change[NSKeyValueChangeNewKey];
        self.title = newName;
    }
}

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}
@end

在这个例子中,当 Person 对象的 name 属性发生变化时,视图控制器的标题会自动更新。

5. KVO 注意事项

5.1 内存管理

正如前面提到的,移除观察者是非常重要的。如果在被观察对象释放之前没有移除观察者,观察者可能会尝试访问已经释放的被观察对象,导致程序崩溃。特别是在使用视图控制器等有生命周期的对象作为观察者时,要在合适的时机(如 dealloc 方法中)移除观察者。

5.2 线程安全

KVO 本身并不是线程安全的。如果在多线程环境中使用 KVO,需要注意同步问题。例如,在一个多线程应用中,可能会出现多个线程同时修改被观察对象属性的情况,这可能会导致观察者收到混乱的通知。为了确保线程安全,可以使用锁(如 NSLockdispatch_semaphore 等)来保护对被观察对象属性的修改操作。

5.3 观察属性的可观察性

不是所有的属性都可以直接进行 KVO 观察。例如,对于只读属性(只有 getter 方法,没有 setter 方法),默认情况下是不能进行 KVO 观察的。要观察只读属性,可能需要通过其他方式,如创建一个代理属性,在代理属性的 setter 方法中触发对只读属性的更新,并通知观察者。

5.4 子类化与 KVO

当对一个类进行子类化时,如果子类重写了被观察属性的 setter 方法,需要确保在重写的 setter 方法中调用 willChangeValueForKey:didChangeValueForKey: 方法,否则 KVO 机制可能无法正常工作。例如:

@interface SubPerson : Person
@end

@implementation SubPerson
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}
@end

在这个例子中,SubPerson 类重写了 setName: 方法,并正确调用了 KVO 相关方法,以确保 KVO 机制在子类中也能正常工作。

6. 代码示例汇总

6.1 基本 KVO 示例

// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

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

@implementation Person
@end

// MyObserver.h
#import <Foundation/Foundation.h>

@interface MyObserver : NSObject
@end

// MyObserver.m
#import "MyObserver.h"

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSString *newName = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's name has changed to: %@", newName);
    }
}
@end

// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "MyObserver.h"

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

6.2 观察集合属性示例

// Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

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

@implementation Person
@end

// Company.h
#import <Foundation/Foundation.h>
#import "Person.h"

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

// Company.m
#import "Company.h"

@implementation Company
@end

// MyObserver.h
#import <Foundation/Foundation.h>

@interface MyObserver : NSObject
@end

// MyObserver.m
#import "MyObserver.h"

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"employees"]) {
        NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue];
        switch (kind) {
            case NSKeyValueChangeInsertion: {
                NSArray *newEmployees = change[NSKeyValueChangeNewKey];
                NSLog(@"New employees added: %@", newEmployees);
                break;
            }
            case NSKeyValueChangeRemoval: {
                NSArray *removedEmployees = change[NSKeyValueChangeOldKey];
                NSLog(@"Employees removed: %@", removedEmployees);
                break;
            }
            case NSKeyValueChangeReplacement: {
                NSArray *oldEmployees = change[NSKeyValueChangeOldKey];
                NSArray *newEmployees = change[NSKeyValueChangeNewKey];
                NSLog(@"Employees replaced: old - %@, new - %@", oldEmployees, newEmployees);
                break;
            }
            default:
                break;
        }
    }
}
@end

// main.m
#import <Foundation/Foundation.h>
#import "Company.h"
#import "MyObserver.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Company *company = [[Company alloc] init];
        MyObserver *observer = [[MyObserver alloc] init];
        [company addObserver:observer forKeyPath:@"employees" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionPrior context:nil];
        
        Person *person1 = [[Person alloc] init];
        person1.name = @"Alice";
        Person *person2 = [[Person alloc] init];
        person2.name = @"Bob";
        
        company.employees = @[person1, person2];
        
        NSMutableArray *mutableEmployees = [company.employees mutableCopy];
        [mutableEmployees removeObjectAtIndex:0];
        company.employees = mutableEmployees;
        
        [company removeObserver:observer forKeyPath:@"employees"];
    }
    return 0;
}

6.3 使用上下文区分多观察示例

// Person.h
#import <Foundation/Foundation.h>

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

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

@implementation Person
@end

// MyObserver.h
#import <Foundation/Foundation.h>

@interface MyObserver : NSObject
@end

// MyObserver.m
#import "MyObserver.h"

static void *PersonNameContext = &PersonNameContext;
static void *PersonAgeContext = &PersonAgeContext;

@implementation MyObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSString *newName = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's name has changed to: %@", newName);
    } else if (context == PersonAgeContext) {
        NSNumber *newAge = change[NSKeyValueChangeNewKey];
        NSLog(@"The person's age has changed to: %@", newAge);
    }
}
@end

// main.m
#import <Foundation/Foundation.h>
#import "Person.h"
#import "MyObserver.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        MyObserver *observer = [[MyObserver alloc] init];
        [person addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
        [person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:PersonAgeContext];
        
        person.name = @"Charlie";
        person.age = 30;
        
        [person removeObserver:observer forKeyPath:@"name"];
        [person removeObserver:observer forKeyPath:@"age"];
    }
    return 0;
}

通过以上内容,我们全面深入地了解了 Objective - C 中 KVO 的原理与应用,包括其基础概念、原理剖析、使用步骤、高级应用以及注意事项,并通过丰富的代码示例进行了详细说明。希望这些知识能帮助开发者在实际项目中更好地运用 KVO 机制,实现高效、灵活的对象间通信和数据变化监控。