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

学会在Objective-C中使用KVO进行属性变化监听

2021-05-197.8k 阅读

一、KVO 简介

KVO 即 Key - Value Observing,是一种基于观察者模式的机制。在 Objective - C 编程中,它允许开发者监听特定对象的属性值变化。当被观察对象的指定属性值发生改变时,系统会自动通知观察者,从而使观察者能够做出相应的响应。

KVO 的核心原理在于运行时系统动态生成一个被观察对象的子类,这个子类重写了被观察属性的 setter 方法。当属性值发生变化时,会调用这个重写的 setter 方法,进而触发通知机制,告知观察者属性值已改变。

二、KVO 的使用步骤

  1. 定义被观察对象 首先,我们需要定义一个类,其属性将被观察。例如,创建一个 Person 类,有一个 name 属性:
#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end
  1. 注册观察者 在需要监听 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,用于在多个观察中区分不同的观察情况。

  1. 实现观察回调方法 注册观察者后,需要在观察者类中实现 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);
    }
}

在这个方法中,首先通过 keyPathobject 来确定是哪个对象的哪个属性发生了变化。然后从 change 字典中获取新值(如果在注册时指定了 NSKeyValueObservingOptionNew)或旧值(如果指定了 NSKeyValueObservingOptionOld)。

  1. 移除观察者 当观察者不再需要监听属性变化时,需要移除观察者,以避免内存泄漏。一般在 dealloc 方法中进行移除操作:
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"name"];
}

removeObserver:forKeyPath: 方法用于移除指定的观察者对特定属性路径的观察。

三、KVO 的深入应用

  1. 观察复杂对象属性 如果被观察对象的属性是一个复杂对象,例如一个自定义的 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);
    }
}
  1. 集合属性的观察 对于集合属性,如 NSArrayNSMutableArray,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 的注意事项

  1. 内存管理 如果观察者对象先于被观察对象释放,而没有及时移除观察者,会导致野指针错误。因此,一定要在合适的时机(如 dealloc 方法)移除观察者。
  2. 线程安全 KVO 本身不是线程安全的。如果在多线程环境下使用 KVO,需要注意同步问题。例如,可以在观察回调方法中使用 dispatch_syncdispatch_async 来确保在主线程或指定的队列中处理属性变化。
  3. KVO 与 KVC 的关系 KVO 是基于 KVC(Key - Value Coding)实现的。KVC 提供了一种通过键值对来访问对象属性的机制,KVO 则在此基础上实现了属性变化的监听。在使用 KVO 时,理解 KVC 的原理和使用方法有助于更好地掌握 KVO。
  4. 自定义类的属性观察 对于自定义类的属性,要确保属性遵循 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
  1. 观察系统类属性 虽然可以观察一些系统类的属性,但并不是所有系统类的属性都支持 KVO。例如,UIViewframe 属性就不支持直接观察。如果需要监听 UIViewframe 变化,可以通过其他方式,如继承 UIView 并重写 setFrame: 方法来手动触发通知。

五、KVO 的替代方案

  1. 代理模式 代理模式是一种常见的替代 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";
  1. 通知中心(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 的优势与适用场景

  1. 优势
    • 简洁性:KVO 提供了一种简洁的方式来监听对象属性的变化。只需要注册观察者和实现观察回调方法,就可以自动接收属性变化的通知,无需在被观察对象的属性 setter 方法中手动添加通知逻辑。
    • 松耦合:观察者和被观察对象之间的耦合度相对较低。被观察对象不需要知道具体的观察者是谁,只需要专注于自身属性的变化。观察者也只需要关心特定属性的变化,而不需要了解被观察对象的其他细节。
    • 灵活性:KVO 支持观察各种类型的属性,包括基本数据类型、对象类型以及集合类型。同时,通过设置不同的观察选项,可以获取属性变化的新值、旧值等信息,满足不同的需求。
  2. 适用场景
    • 数据模型与视图的绑定:在 MVC(Model - View - Controller)架构中,KVO 常用于数据模型(Model)和视图(View)之间的绑定。当数据模型的属性发生变化时,通过 KVO 通知视图进行更新,实现数据与界面的同步。
    • 状态监控:对于一些需要监控对象状态变化的场景,KVO 非常适用。例如,监控网络连接状态、用户登录状态等属性的变化,以便及时做出相应的处理。
    • 复杂对象关系管理:在处理复杂对象关系时,KVO 可以方便地监听嵌套对象的属性变化。例如,在一个包含多个层次对象的应用中,通过 KVO 可以快速响应深层对象属性的改变。

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

  1. 音乐播放应用 在一个音乐播放应用中,有一个 MusicPlayer 类,其有 currentSong 属性,代表当前正在播放的歌曲。currentSong 是一个自定义的 Song 类对象,Song 类有 titleartist 等属性。
#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.titlecurrentSong.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

这样,当 MusicPlayercurrentSong 属性的 titleartist 发生变化时,播放界面能够及时更新显示。

  1. 电商购物车应用 在电商购物车应用中,有一个 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 集合的变化以及 CartItemquantityprice 属性变化,以便实时更新购物车总价等信息:

#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 可以提高代码的可维护性和灵活性,打造出更加健壮和高效的应用程序。