学会在Objective-C中使用KVO进行属性变化监听
一、KVO 简介
KVO 即 Key - Value Observing,是一种基于观察者模式的机制。在 Objective - C 编程中,它允许开发者监听特定对象的属性值变化。当被观察对象的指定属性值发生改变时,系统会自动通知观察者,从而使观察者能够做出相应的响应。
KVO 的核心原理在于运行时系统动态生成一个被观察对象的子类,这个子类重写了被观察属性的 setter 方法。当属性值发生变化时,会调用这个重写的 setter 方法,进而触发通知机制,告知观察者属性值已改变。
二、KVO 的使用步骤
- 定义被观察对象
首先,我们需要定义一个类,其属性将被观察。例如,创建一个
Person
类,有一个name
属性:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
- 注册观察者
在需要监听
Person
对象name
属性变化的地方,注册观察者。假设在ViewController
中进行监听:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
// 注册观察者,self 为观察者,@"name" 为观察的属性,options 为观察选项
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
这里的 addObserver:forKeyPath:options:context:
方法是注册观察者的关键。addObserver:
参数指定观察者对象,forKeyPath:
参数指定要观察的属性路径,options:
参数用于指定观察选项,常见的有 NSKeyValueObservingOptionNew
(获取新值)、NSKeyValueObservingOptionOld
(获取旧值) 等,context:
参数一般传 nil
,用于在多个观察中区分不同的观察情况。
- 实现观察回调方法
注册观察者后,需要在观察者类中实现
observeValueForKeyPath:ofObject:change:context:
方法,当被观察属性值发生变化时,系统会调用这个方法。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"] && object == self.person) {
NSString *newName = change[NSKeyValueChangeNewKey];
NSLog(@"The person's name has changed to: %@", newName);
}
}
在这个方法中,首先通过 keyPath
和 object
来确定是哪个对象的哪个属性发生了变化。然后从 change
字典中获取新值(如果在注册时指定了 NSKeyValueObservingOptionNew
)或旧值(如果指定了 NSKeyValueObservingOptionOld
)。
- 移除观察者
当观察者不再需要监听属性变化时,需要移除观察者,以避免内存泄漏。一般在
dealloc
方法中进行移除操作:
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
removeObserver:forKeyPath:
方法用于移除指定的观察者对特定属性路径的观察。
三、KVO 的深入应用
- 观察复杂对象属性
如果被观察对象的属性是一个复杂对象,例如一个自定义的
Book
类,且Book
类有title
属性,我们同样可以进行观察。 首先定义Book
类:
#import <Foundation/Foundation.h>
@interface Book : NSObject
@property (nonatomic, strong) NSString *title;
@end
@implementation Book
@end
然后在 Person
类中添加 Book
属性:
#import <Foundation/Foundation.h>
#import "Book.h"
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) Book *book;
@end
@implementation Person
@end
在观察者中注册对 book.title
的观察:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person.book = [[Book alloc] init];
// 注册对 book.title 的观察
[self.person addObserver:self forKeyPath:@"book.title" options:NSKeyValueObservingOptionNew context:nil];
}
并在观察回调方法中处理:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"book.title"] && object == self.person) {
NSString *newTitle = change[NSKeyValueChangeNewKey];
NSLog(@"The book's title has changed to: %@", newTitle);
}
}
- 集合属性的观察
对于集合属性,如
NSArray
或NSMutableArray
,KVO 提供了特殊的处理方式。假设Person
类有一个friends
属性,是一个NSMutableArray
:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSMutableArray<Person *> *friends;
@end
@implementation Person
@end
在注册观察时,需要使用 NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior
选项,并且在回调方法中进行不同的处理:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.person.friends = [NSMutableArray array];
// 注册对 friends 集合的观察
[self.person addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"friends"] && object == self.person) {
NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue];
switch (kind) {
case NSKeyValueChangeInsertion: {
NSArray *newFriends = change[NSKeyValueChangeNewKey];
NSLog(@"New friends added: %@", newFriends);
break;
}
case NSKeyValueChangeRemoval: {
NSArray *removedFriends = change[NSKeyValueChangeOldKey];
NSLog(@"Friends removed: %@", removedFriends);
break;
}
case NSKeyValueChangeReplacement: {
NSArray *oldFriends = change[NSKeyValueChangeOldKey];
NSArray *newFriends = change[NSKeyValueChangeNewKey];
NSLog(@"Friends replaced: old - %@, new - %@", oldFriends, newFriends);
break;
}
case NSKeyValueChangeMove: {
NSIndexSet *fromIndexes = change[NSKeyValueChangeOldIndexKey];
NSIndexSet *toIndexes = change[NSKeyValueChangeNewIndexKey];
NSLog(@"Friends moved from %@ to %@", fromIndexes, toIndexes);
break;
}
default:
break;
}
}
}
这里通过 NSKeyValueChangeKindKey
获取集合变化的类型,然后根据不同的类型进行相应的处理。
四、KVO 的注意事项
- 内存管理
如果观察者对象先于被观察对象释放,而没有及时移除观察者,会导致野指针错误。因此,一定要在合适的时机(如
dealloc
方法)移除观察者。 - 线程安全
KVO 本身不是线程安全的。如果在多线程环境下使用 KVO,需要注意同步问题。例如,可以在观察回调方法中使用
dispatch_sync
或dispatch_async
来确保在主线程或指定的队列中处理属性变化。 - KVO 与 KVC 的关系 KVO 是基于 KVC(Key - Value Coding)实现的。KVC 提供了一种通过键值对来访问对象属性的机制,KVO 则在此基础上实现了属性变化的监听。在使用 KVO 时,理解 KVC 的原理和使用方法有助于更好地掌握 KVO。
- 自定义类的属性观察
对于自定义类的属性,要确保属性遵循 KVO 的要求。如果属性没有使用
@synthesize
自动合成,需要手动实现 setter 方法,并且在 setter 方法中调用willChangeValueForKey:
和didChangeValueForKey:
方法,以触发 KVO 通知。例如:
#import <Foundation/Foundation.h>
@interface CustomClass : NSObject
@property (nonatomic, assign) int customValue;
@end
@implementation CustomClass
- (void)setCustomValue:(int)customValue {
[self willChangeValueForKey:@"customValue"];
_customValue = customValue;
[self didChangeValueForKey:@"customValue"];
}
@end
- 观察系统类属性
虽然可以观察一些系统类的属性,但并不是所有系统类的属性都支持 KVO。例如,
UIView
的frame
属性就不支持直接观察。如果需要监听UIView
的frame
变化,可以通过其他方式,如继承UIView
并重写setFrame:
方法来手动触发通知。
五、KVO 的替代方案
- 代理模式
代理模式是一种常见的替代 KVO 的方式。在代理模式中,被观察对象持有一个代理对象的引用,当属性发生变化时,被观察对象调用代理对象的特定方法来通知。例如,定义一个
PersonDelegate
协议:
#import <Foundation/Foundation.h>
@protocol PersonDelegate <NSObject>
- (void)personNameDidChange:(Person *)person;
@end
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, weak) id<PersonDelegate> delegate;
@end
@implementation Person
- (void)setName:(NSString *)name {
_name = name;
if ([self.delegate respondsToSelector:@selector(personNameDidChange:)]) {
[self.delegate personNameDidChange:self];
}
}
@end
在使用时,创建一个遵守 PersonDelegate
协议的类,并设置为 Person
对象的代理:
@interface DelegateClass : NSObject <PersonDelegate>
@end
@implementation DelegateClass
- (void)personNameDidChange:(Person *)person {
NSLog(@"The person's name has changed: %@", person.name);
}
@end
// 在其他地方使用
DelegateClass *delegate = [[DelegateClass alloc] init];
Person *person = [[Person alloc] init];
person.delegate = delegate;
person.name = @"New Name";
- 通知中心(NSNotificationCenter)
通知中心也是一种可以实现类似功能的机制。被观察对象在属性变化时,通过
NSNotificationCenter
发送通知,观察者注册接收该通知并处理。例如:
// Person 类中发送通知
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
- (void)setName:(NSString *)name {
_name = name;
[[NSNotificationCenter defaultCenter] postNotificationName:@"PersonNameDidChangeNotification" object:self userInfo:@{@"newName": name}];
}
@end
// 观察者接收通知
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(personNameDidChange:) name:@"PersonNameDidChangeNotification" object:self.person];
}
- (void)personNameDidChange:(NSNotification *)notification {
NSString *newName = notification.userInfo[@"newName"];
NSLog(@"The person's name has changed to: %@", newName);
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"PersonNameDidChangeNotification" object:self.person];
}
@end
代理模式和通知中心各有优缺点。代理模式更加直接,适用于一对一的关系;通知中心则更灵活,适用于一对多的关系,但可能会导致代码耦合度增加。相比之下,KVO 提供了一种相对简洁、自动的属性变化监听机制,适用于很多场景,但需要注意其使用的细节和限制。
六、总结 KVO 的优势与适用场景
- 优势
- 简洁性:KVO 提供了一种简洁的方式来监听对象属性的变化。只需要注册观察者和实现观察回调方法,就可以自动接收属性变化的通知,无需在被观察对象的属性 setter 方法中手动添加通知逻辑。
- 松耦合:观察者和被观察对象之间的耦合度相对较低。被观察对象不需要知道具体的观察者是谁,只需要专注于自身属性的变化。观察者也只需要关心特定属性的变化,而不需要了解被观察对象的其他细节。
- 灵活性:KVO 支持观察各种类型的属性,包括基本数据类型、对象类型以及集合类型。同时,通过设置不同的观察选项,可以获取属性变化的新值、旧值等信息,满足不同的需求。
- 适用场景
- 数据模型与视图的绑定:在 MVC(Model - View - Controller)架构中,KVO 常用于数据模型(Model)和视图(View)之间的绑定。当数据模型的属性发生变化时,通过 KVO 通知视图进行更新,实现数据与界面的同步。
- 状态监控:对于一些需要监控对象状态变化的场景,KVO 非常适用。例如,监控网络连接状态、用户登录状态等属性的变化,以便及时做出相应的处理。
- 复杂对象关系管理:在处理复杂对象关系时,KVO 可以方便地监听嵌套对象的属性变化。例如,在一个包含多个层次对象的应用中,通过 KVO 可以快速响应深层对象属性的改变。
七、KVO 在实际项目中的应用案例
- 音乐播放应用
在一个音乐播放应用中,有一个
MusicPlayer
类,其有currentSong
属性,代表当前正在播放的歌曲。currentSong
是一个自定义的Song
类对象,Song
类有title
、artist
等属性。
#import <Foundation/Foundation.h>
@interface Song : NSObject
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *artist;
@end
@implementation Song
@end
@interface MusicPlayer : NSObject
@property (nonatomic, strong) Song *currentSong;
@end
@implementation MusicPlayer
@end
在播放界面的视图控制器(MusicPlayerViewController
)中,使用 KVO 监听 currentSong.title
和 currentSong.artist
的变化,以便实时更新界面显示:
#import "MusicPlayerViewController.h"
#import "MusicPlayer.h"
#import "Song.h"
@interface MusicPlayerViewController ()
@property (nonatomic, strong) MusicPlayer *musicPlayer;
@end
@implementation MusicPlayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.musicPlayer = [[MusicPlayer alloc] init];
// 监听 currentSong.title
[self.musicPlayer addObserver:self forKeyPath:@"currentSong.title" options:NSKeyValueObservingOptionNew context:nil];
// 监听 currentSong.artist
[self.musicPlayer addObserver:self forKeyPath:@"currentSong.artist" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.musicPlayer) {
if ([keyPath isEqualToString:@"currentSong.title"]) {
NSString *newTitle = change[NSKeyValueChangeNewKey];
// 更新界面上歌曲标题的显示
self.songTitleLabel.text = newTitle;
} else if ([keyPath isEqualToString:@"currentSong.artist"]) {
NSString *newArtist = change[NSKeyValueChangeNewKey];
// 更新界面上歌手名称的显示
self.artistLabel.text = newArtist;
}
}
}
- (void)dealloc {
[self.musicPlayer removeObserver:self forKeyPath:@"currentSong.title"];
[self.musicPlayer removeObserver:self forKeyPath:@"currentSong.artist"];
}
@end
这样,当 MusicPlayer
的 currentSong
属性的 title
或 artist
发生变化时,播放界面能够及时更新显示。
- 电商购物车应用
在电商购物车应用中,有一个
Cart
类,代表购物车。Cart
类有一个items
属性,是一个NSMutableArray
,存储购物车中的商品项(CartItem
对象)。CartItem
类有quantity
(数量)和price
(价格)等属性。
#import <Foundation/Foundation.h>
@interface CartItem : NSObject
@property (nonatomic, assign) NSInteger quantity;
@property (nonatomic, assign) CGFloat price;
@end
@implementation CartItem
@end
@interface Cart : NSObject
@property (nonatomic, strong) NSMutableArray<CartItem *> *items;
@end
@implementation Cart
@end
在购物车界面的视图控制器(CartViewController
)中,使用 KVO 监听 items
集合的变化以及 CartItem
的 quantity
和 price
属性变化,以便实时更新购物车总价等信息:
#import "CartViewController.h"
#import "Cart.h"
#import "CartItem.h"
@interface CartViewController ()
@property (nonatomic, strong) Cart *cart;
@end
@implementation CartViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.cart = [[Cart alloc] init];
self.cart.items = [NSMutableArray array];
// 监听 items 集合变化
[self.cart addObserver:self forKeyPath:@"items" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior context:nil];
for (CartItem *item in self.cart.items) {
// 监听每个 CartItem 的 quantity 变化
[item addObserver:self forKeyPath:@"quantity" options:NSKeyValueObservingOptionNew context:nil];
// 监听每个 CartItem 的 price 变化
[item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self.cart && [keyPath isEqualToString:@"items"]) {
NSKeyValueChange kind = [change[NSKeyValueChangeKindKey] integerValue];
switch (kind) {
case NSKeyValueChangeInsertion: {
NSArray *newItems = change[NSKeyValueChangeNewKey];
for (CartItem *item in newItems) {
[item addObserver:self forKeyPath:@"quantity" options:NSKeyValueObservingOptionNew context:nil];
[item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew context:nil];
}
// 更新购物车界面显示,如添加新商品项的展示
[self updateCartUI];
break;
}
case NSKeyValueChangeRemoval: {
NSArray *removedItems = change[NSKeyValueChangeOldKey];
for (CartItem *item in removedItems) {
[item removeObserver:self forKeyPath:@"quantity"];
[item removeObserver:self forKeyPath:@"price"];
}
// 更新购物车界面显示,如移除商品项的展示
[self updateCartUI];
break;
}
default:
break;
}
} else if ([object isKindOfClass:[CartItem class]]) {
if ([keyPath isEqualToString:@"quantity"] || [keyPath isEqualToString:@"price"]) {
// 更新购物车总价等信息
[self updateCartTotalPrice];
}
}
}
- (void)updateCartUI {
// 具体的界面更新逻辑,如重新加载商品列表等
}
- (void)updateCartTotalPrice {
CGFloat totalPrice = 0;
for (CartItem *item in self.cart.items) {
totalPrice += item.quantity * item.price;
}
// 更新界面上总价的显示
self.totalPriceLabel.text = [NSString stringWithFormat:@"Total: %.2f", totalPrice];
}
- (void)dealloc {
[self.cart removeObserver:self forKeyPath:@"items"];
for (CartItem *item in self.cart.items) {
[item removeObserver:self forKeyPath:@"quantity"];
[item removeObserver:self forKeyPath:@"price"];
}
}
@end
通过这种方式,在购物车的商品项数量、价格变化或商品项添加、移除时,购物车界面能够实时更新相关信息。
通过以上详细的介绍、代码示例以及实际应用案例,相信读者对在 Objective - C 中使用 KVO 进行属性变化监听有了全面而深入的理解。在实际开发中,根据具体的需求和场景,合理地运用 KVO 可以提高代码的可维护性和灵活性,打造出更加健壮和高效的应用程序。