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

了解Objective-C中KVO(键值观察)的语法与实现

2022-12-152.0k 阅读

一、KVO基础概念

在Objective-C编程中,KVO(Key-Value Observing)即键值观察,是一种基于观察者模式的设计模式。它允许一个对象观察另一个对象特定属性值的变化。当被观察对象的属性值发生改变时,系统会自动通知观察者对象,使得观察者能够相应地作出处理。这种机制在构建响应式用户界面以及实现对象间松耦合交互方面非常有用。

例如,在一个用户登录界面,当用户输入用户名和密码后,登录按钮的可用性可能依赖于用户名和密码输入框是否都有值。此时,可以使用KVO来观察用户名和密码输入框的文本属性,当它们都有值时,自动启用登录按钮。

二、KVO的语法

  1. 注册观察 在Objective-C中,要使用KVO,首先需要在观察者对象中注册对被观察对象特定属性的观察。这通过addObserver:forKeyPath:options:context:方法来实现。
  • addObserver::指定观察者对象,通常是self
  • forKeyPath::以字符串形式指定被观察对象的属性路径。例如,如果有一个Person类有name属性,这里就写@"name"
  • options::指定观察选项,常见的有NSKeyValueObservingOptionNew(获取新值)和NSKeyValueObservingOptionOld(获取旧值),可以通过按位或操作组合使用。
  • context::一个上下文指针,通常是一个静态变量,用于在观察回调中区分不同的观察。

示例代码如下:

// 假设我们有一个Person类
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

// 在另一个类中观察Person的name属性
@interface Observer : NSObject
@end

@implementation Observer
- (void)startObserving {
    Person *person = [[Person alloc] init];
    static void *MyContext = &MyContext;
    [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:MyContext];
    person.name = @"John";
}
@end
  1. 实现观察回调 当被观察对象的属性值发生变化时,系统会调用观察者对象的observeValueForKeyPath:ofObject:change:context:方法。
  • observeValueForKeyPath::被观察的属性路径。
  • ofObject::被观察的对象。
  • change::一个字典,包含了属性值变化的相关信息,例如新值(NSKeyValueChangeNewKey)和旧值(NSKeyValueChangeOldKey)。
  • context::注册观察时传入的上下文指针。

继续上面的示例,我们在Observer类中实现observeValueForKeyPath:ofObject:change:context:方法:

@implementation Observer
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    static void *MyContext = &MyContext;
    if (context == MyContext) {
        if ([keyPath isEqualToString:@"name"]) {
            NSString *oldValue = change[NSKeyValueChangeOldKey];
            NSString *newValue = change[NSKeyValueChangeNewKey];
            NSLog(@"Name changed from %@ to %@", oldValue, newValue);
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end
  1. 移除观察 当观察者不再需要观察被观察对象的属性变化时,需要调用removeObserver:forKeyPath:方法移除观察。这是非常重要的,否则可能会导致内存泄漏。 还是以上面的代码为例,在Observer类中添加移除观察的方法:
@implementation Observer
- (void)stopObserving {
    Person *person = [[Person alloc] init];
    [person removeObserver:self forKeyPath:@"name"];
}
@end

三、KVO的实现本质

  1. 动态创建子类 当一个对象注册为另一个对象的观察者时,系统会动态创建被观察对象的子类。这个子类是在运行时生成的,并且会重写被观察属性的setter方法。例如,如果被观察对象Person有一个name属性,动态生成的子类会重写setName:方法。
  2. 方法交换 在动态生成的子类中,系统通过方法交换(Method Swizzling)技术,将原setter方法替换为一个新的方法。新的setter方法在调用原setter方法设置属性值后,会通知所有注册的观察者属性值发生了变化。
  3. isa指针的变化 被观察对象的isa指针会在运行时指向动态生成的子类。这样,当调用被观察对象的setter方法时,实际上调用的是动态子类重写后的setter方法。例如:
Person *person = [[Person alloc] init];
NSLog(@"Original class: %@", NSStringFromClass([person class]));
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
NSLog(@"Class after adding observer: %@", NSStringFromClass([person class]));

上述代码在添加观察者前后打印对象的类,可以看到类发生了变化,变为了动态生成的子类。

四、KVO的注意事项

  1. 线程安全 KVO的通知是在主线程中发送的。如果被观察对象的属性值在子线程中发生变化,通知仍然会在主线程中发送。这意味着在观察者的回调方法中更新UI等操作是安全的,但如果需要在回调中进行一些耗时操作,可能需要将这些操作放到子线程中执行,以避免阻塞主线程。
  2. 属性的一致性 被观察对象的属性必须遵循KVO的约定。如果属性是通过自定义的setter方法来设置值,那么这个setter方法必须能够正确地触发KVO通知。例如,对于一个简单的属性name,如果手动实现了setName:方法,需要确保在设置_name成员变量后,调用willChangeValueForKey:didChangeValueForKey:方法。
@implementation Person
- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
@end
  1. 循环引用 在使用KVO时,要注意避免循环引用。例如,如果A对象观察B对象的属性,同时B对象又观察A对象的属性,就可能会导致循环引用,从而引起内存泄漏。可以通过合理设计对象的生命周期,或者使用弱引用(weak)来解决这个问题。

五、复杂场景下的KVO应用

  1. 多层级对象观察 有时,被观察对象的属性可能是另一个对象,而我们需要观察这个嵌套对象的属性。例如,有一个Company类,它有一个CEO属性,而CEOPerson类的实例,我们想要观察CEOname属性。
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

@implementation Person
@end

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

@implementation Company
@end

@interface Observer : NSObject
@end

@implementation Observer
- (void)startObserving {
    Company *company = [[Company alloc] init];
    Person *ceo = [[Person alloc] init];
    company.CEO = ceo;
    static void *MyContext = &MyContext;
    [company addObserver:self forKeyPath:@"CEO.name" options:NSKeyValueObservingOptionNew context:MyContext];
    ceo.name = @"Alice";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    static void *MyContext = &MyContext;
    if (context == MyContext) {
        if ([keyPath isEqualToString:@"CEO.name"]) {
            NSString *newValue = change[NSKeyValueChangeNewKey];
            NSLog(@"CEO's name changed to %@", newValue);
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end
  1. 数组属性观察 当被观察对象的属性是一个数组时,情况会稍微复杂一些。因为数组的一些操作(如添加、删除元素)不会直接触发KVO通知。为了观察数组属性的变化,需要使用NSKeyValueObservingOptionPrior选项,并在observeValueForKeyPath:ofObject:change:context:方法中进行特殊处理。
@interface Team : NSObject
@property (nonatomic, strong) NSMutableArray *members;
@end

@implementation Team
@end

@interface Observer : NSObject
@end

@implementation Observer
- (void)startObserving {
    Team *team = [[Team alloc] init];
    team.members = [NSMutableArray arrayWithObjects:@"Bob", @"Charlie", nil];
    static void *MyContext = &MyContext;
    [team addObserver:self forKeyPath:@"members" options:NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionNew context:MyContext];
    [team.members addObject:@"David"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    static void *MyContext = &MyContext;
    if (context == MyContext) {
        if ([keyPath isEqualToString:@"members"]) {
            NSKeyValueChange changeKind = [change[NSKeyValueChangeKindKey] integerValue];
            if (changeKind == NSKeyValueChangeInsertion) {
                NSArray *newMembers = change[NSKeyValueChangeNewKey];
                NSLog(@"New members added: %@", newMembers);
            }
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end

六、与其他设计模式的结合

  1. 与MVC模式结合 在MVC(Model-View-Controller)模式中,KVO可以很好地用于实现视图(View)与模型(Model)之间的通信。模型中的数据变化可以通过KVO通知视图,使得视图能够及时更新显示。例如,在一个待办事项列表应用中,待办事项数据(模型)的添加、删除或完成状态的改变可以通过KVO通知视图,让视图刷新列表显示。
  2. 与MVVM模式结合 在MVVM(Model-View-ViewModel)模式中,KVO同样扮演着重要角色。视图模型(ViewModel)可以观察模型的属性变化,并将这些变化转化为适合视图使用的形式。同时,视图可以观察视图模型的属性,实现数据的双向绑定。例如,在一个登录页面,视图模型观察用户名和密码输入框的文本变化(通过KVO),并根据输入情况更新登录按钮的可用性属性,视图则观察视图模型的登录按钮可用性属性来决定按钮的显示状态。

七、KVO的性能考量

  1. 频繁通知的影响 如果被观察对象的属性值频繁变化,会导致KVO通知频繁发送。这可能会影响性能,特别是在性能敏感的场景下,如游戏开发中的实时渲染。在这种情况下,可以考虑批量处理KVO通知,或者使用更细粒度的控制来减少不必要的通知。
  2. 动态子类创建开销 由于KVO会动态创建被观察对象的子类,这会带来一定的性能开销,特别是在创建大量被观察对象时。在设计时,需要权衡KVO带来的便利性与性能开销,考虑是否有其他更轻量级的方式来实现相同的功能。

八、在实际项目中的应用案例

  1. 社交应用中的好友状态跟踪 在一个社交应用中,用户可以添加好友并关注好友的状态。可以使用KVO来观察好友对象的状态属性(如“在线”“离线”“忙碌”等)。当好友状态发生变化时,系统可以及时通知用户,例如在好友列表中更新好友的状态显示。
@interface Friend : NSObject
@property (nonatomic, assign) BOOL isOnline;
@end

@implementation Friend
@end

@interface User : NSObject
@end

@implementation User
- (void)trackFriendStatus {
    Friend *friend = [[Friend alloc] init];
    static void *MyContext = &MyContext;
    [friend addObserver:self forKeyPath:@"isOnline" options:NSKeyValueObservingOptionNew context:MyContext];
    friend.isOnline = YES;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    static void *MyContext = &MyContext;
    if (context == MyContext) {
        if ([keyPath isEqualToString:@"isOnline"]) {
            BOOL newStatus = [change[NSKeyValueChangeNewKey] boolValue];
            if (newStatus) {
                NSLog(@"Friend is online");
            } else {
                NSLog(@"Friend is offline");
            }
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end
  1. 金融应用中的账户余额监控 在金融应用中,用户可以监控自己账户余额的变化。通过KVO观察账户对象的余额属性,当余额发生变化时,系统可以通知用户,例如显示余额变化的通知消息,或者更新账户余额的显示界面。
@interface Account : NSObject
@property (nonatomic, assign) double balance;
@end

@implementation Account
@end

@interface User : NSObject
@end

@implementation User
- (void)monitorAccountBalance {
    Account *account = [[Account alloc] init];
    static void *MyContext = &MyContext;
    [account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:MyContext];
    account.balance = 1000.0;
    account.balance = 1200.0;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    static void *MyContext = &MyContext;
    if (context == MyContext) {
        if ([keyPath isEqualToString:@"balance"]) {
            double newBalance = [change[NSKeyValueChangeNewKey] doubleValue];
            NSLog(@"Account balance changed to %f", newBalance);
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
@end

通过以上内容,我们详细了解了Objective-C中KVO的语法、实现本质、注意事项、复杂场景应用、与其他设计模式的结合、性能考量以及在实际项目中的应用案例。KVO是Objective-C编程中一个强大且实用的特性,合理运用它可以大大提高代码的可维护性和灵活性。