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

Objective-C 中的观察者模式运用与场景

2023-10-306.8k 阅读

观察者模式基础概念

在软件系统中,经常会出现这样一种场景:当一个对象的状态发生变化时,需要自动通知其他多个对象,让它们做出相应的反应。例如,在一个新闻发布系统中,当有新的新闻发布时,所有订阅该类新闻的用户都应该收到通知。观察者模式(Observer Pattern)就是为了解决这类问题而诞生的一种设计模式。

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当这个主题对象的状态发生变化时,会自动通知所有的观察者对象,使它们能够自动更新自己的状态。在这种模式中,主题(Subject)是被观察的对象,而观察者(Observer)则是依赖于主题的对象。

Objective-C 中实现观察者模式的基础方式

在Objective-C中,实现观察者模式可以通过使用通知中心(NSNotificationCenter)或者KVO(Key - Value Observing,键值观察)。

使用NSNotificationCenter

NSNotificationCenter是一个基于通知的观察者模式实现。它允许对象注册为某个特定通知的观察者,当该通知被发布时,观察者会收到通知并执行相应的处理代码。

首先,我们创建一个主题类Subject,当主题状态变化时,它会发布通知:

#import <Foundation/Foundation.h>

@interface Subject : NSObject

@property (nonatomic, assign) NSInteger state;

- (void)changeState;

@end

@implementation Subject

- (void)changeState {
    self.state = arc4random_uniform(100);
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName:@"SubjectStateChanged" object:self userInfo:@{@"newState": @(self.state)}];
}

@end

然后,创建一个观察者类Observer,它会注册为SubjectStateChanged通知的观察者:

#import <Foundation/Foundation.h>

@interface Observer : NSObject

- (void)handleNotification:(NSNotification *)notification;

@end

@implementation Observer

- (void)handleNotification:(NSNotification *)notification {
    NSLog(@"Observer received notification. Subject's new state: %@", notification.userInfo[@"newState"]);
}

@end

在使用时,我们在main函数中进行如下操作:

#import <Foundation/Foundation.h>
#import "Subject.h"
#import "Observer.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Subject *subject = [[Subject alloc] init];
        Observer *observer = [[Observer alloc] init];
        
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:observer selector:@selector(handleNotification:) name:@"SubjectStateChanged" object:subject];
        
        [subject changeState];
        
        [center removeObserver:observer name:@"SubjectStateChanged" object:subject];
    }
    return 0;
}

在上述代码中,Subject类通过NSNotificationCenter发布了一个名为SubjectStateChanged的通知,Observer类通过addObserver:selector:name:object:方法注册为该通知的观察者。当SubjectchangeState方法被调用时,通知被发布,ObserverhandleNotification:方法被执行。最后,通过removeObserver:name:object:方法移除观察者,避免内存泄漏。

使用KVO

KVO是一种基于观察者模式的机制,它允许开发者监听对象属性值的变化。

首先,创建一个被观察的类Person

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end

然后,创建一个观察者类Watcher

#import <Foundation/Foundation.h>

@interface Watcher : NSObject

@end

@implementation Watcher

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"] && object != nil) {
        NSLog(@"The name of the person has changed to: %@", change[NSKeyValueChangeNewKey]);
    }
}

@end

main函数中使用KVO:

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

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

在上述代码中,Watcher类通过addObserver:forKeyPath:options:context:方法注册为Personname属性的观察者。当personname属性值发生变化时,WatcherobserveValueForKeyPath:ofObject:change:context:方法会被调用。最后,通过removeObserver:forKeyPath:方法移除观察者。

观察者模式在iOS开发中的常见场景

界面更新场景

在iOS应用开发中,界面更新是一个非常常见的场景。例如,当用户在设置界面中更改了应用的主题颜色,应用的各个界面都需要相应地更新以显示新的主题颜色。

假设我们有一个ThemeManager类作为主题状态的主题对象:

#import <Foundation/Foundation.h>

@interface ThemeManager : NSObject

@property (nonatomic, strong) UIColor *themeColor;

+ (instancetype)sharedManager;

- (void)changeThemeColor;

@end

@implementation ThemeManager

+ (instancetype)sharedManager {
    static ThemeManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManager = [[ThemeManager alloc] init];
    });
    return sharedManager;
}

- (void)changeThemeColor {
    self.themeColor = [UIColor redColor];
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName:@"ThemeColorChanged" object:self userInfo:@{@"newColor": self.themeColor}];
}

@end

然后,有一个ViewController类作为观察者,监听主题颜色变化并更新界面:

#import "ViewController.h"
#import "ThemeManager.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(updateTheme) name:@"ThemeColorChanged" object:[ThemeManager sharedManager]];
}

- (void)updateTheme {
    ThemeManager *manager = [ThemeManager sharedManager];
    self.view.backgroundColor = manager.themeColor;
}

- (void)dealloc {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:@"ThemeColorChanged" object:[ThemeManager sharedManager]];
}

@end

在上述代码中,ThemeManager类管理应用的主题颜色,当主题颜色变化时,它发布ThemeColorChanged通知。ViewController类注册为该通知的观察者,当收到通知时,它会更新视图的背景颜色。

数据模型变化通知场景

在一个具有数据模型的应用中,例如一个待办事项应用,当待办事项的数据模型发生变化时,如新增、删除或修改待办事项,相关的视图需要及时更新。

假设我们有一个TodoList类作为数据模型主题对象:

#import <Foundation/Foundation.h>

@interface Todo : NSObject

@property (nonatomic, strong) NSString *title;
@property (nonatomic, assign) BOOL completed;

@end

@implementation Todo

@end

@interface TodoList : NSObject

@property (nonatomic, strong) NSMutableArray<Todo *> *todos;

+ (instancetype)sharedList;

- (void)addTodo:(Todo *)todo;
- (void)removeTodoAtIndex:(NSUInteger)index;

@end

@implementation TodoList

+ (instancetype)sharedList {
    static TodoList *sharedList = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedList = [[TodoList alloc] init];
        sharedList.todos = [NSMutableArray array];
    });
    return sharedList;
}

- (void)addTodo:(Todo *)todo {
    [self.todos addObject:todo];
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName:@"TodoListChanged" object:self userInfo:@{@"changeType": @"add", @"newTodo": todo}];
}

- (void)removeTodoAtIndex:(NSUInteger)index {
    if (index < self.todos.count) {
        Todo *todo = self.todos[index];
        [self.todos removeObjectAtIndex:index];
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center postNotificationName:@"TodoListChanged" object:self userInfo:@{@"changeType": @"remove", @"removedTodo": todo}];
    }
}

@end

然后,有一个TodoViewController类作为观察者,监听待办事项列表变化并更新界面:

#import "TodoViewController.h"
#import "TodoList.h"
#import "Todo.h"

@interface TodoViewController ()

@property (nonatomic, strong) UITableView *tableView;

@end

@implementation TodoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    [self.view addSubview:self.tableView];
    
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(updateTableView) name:@"TodoListChanged" object:[TodoList sharedList]];
}

- (void)updateTableView {
    [self.tableView reloadData];
}

- (void)dealloc {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:@"TodoListChanged" object:[TodoList sharedList]];
}

@end

在上述代码中,TodoList类管理待办事项列表,当列表发生新增或删除操作时,它发布TodoListChanged通知。TodoViewController类注册为该通知的观察者,当收到通知时,它会重新加载表格视图以显示最新的待办事项列表。

网络请求状态通知场景

在应用进行网络请求时,需要实时通知用户请求的状态,如开始请求、请求成功、请求失败等。

假设我们有一个NetworkManager类作为主题对象:

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, NetworkRequestStatus) {
    NetworkRequestStatusStarted,
    NetworkRequestStatusSuccess,
    NetworkRequestStatusFailed
};

@interface NetworkManager : NSObject

+ (instancetype)sharedManager;

- (void)startRequest;

@end

@implementation NetworkManager

+ (instancetype)sharedManager {
    static NetworkManager *sharedManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManager = [[NetworkManager alloc] init];
    });
    return sharedManager;
}

- (void)startRequest {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName:@"NetworkRequestStatusChanged" object:self userInfo:@{@"status": @(NetworkRequestStatusStarted)}];
    
    // 模拟网络请求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        BOOL success = arc4random_uniform(2);
        if (success) {
            [center postNotificationName:@"NetworkRequestStatusChanged" object:self userInfo:@{@"status": @(NetworkRequestStatusSuccess)}];
        } else {
            [center postNotificationName:@"NetworkRequestStatusChanged" object:self userInfo:@{@"status": @(NetworkRequestStatusFailed)}];
        }
    });
}

@end

然后,有一个ViewController类作为观察者,监听网络请求状态变化并更新界面:

#import "ViewController.h"
#import "NetworkManager.h"

@interface ViewController ()

@property (nonatomic, strong) UILabel *statusLabel;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.statusLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 50)];
    [self.view addSubview:self.statusLabel];
    
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self selector:@selector(updateStatus:) name:@"NetworkRequestStatusChanged" object:[NetworkManager sharedManager]];
    
    [[NetworkManager sharedManager] startRequest];
}

- (void)updateStatus:(NSNotification *)notification {
    NSNumber *statusNumber = notification.userInfo[@"status"];
    NetworkRequestStatus status = (NetworkRequestStatus)[statusNumber integerValue];
    switch (status) {
        case NetworkRequestStatusStarted:
            self.statusLabel.text = @"Request started";
            break;
        case NetworkRequestStatusSuccess:
            self.statusLabel.text = @"Request success";
            break;
        case NetworkRequestStatusFailed:
            self.statusLabel.text = @"Request failed";
            break;
        default:
            break;
    }
}

- (void)dealloc {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self name:@"NetworkRequestStatusChanged" object:[NetworkManager sharedManager]];
}

@end

在上述代码中,NetworkManager类管理网络请求,并在请求不同阶段发布NetworkRequestStatusChanged通知。ViewController类注册为该通知的观察者,根据不同的状态更新界面上的状态标签。

深入理解观察者模式在Objective-C中的优缺点

优点

  1. 解耦主题与观察者:主题和观察者之间通过通知或KVO机制进行通信,它们不需要直接引用对方,降低了对象之间的耦合度。例如在待办事项应用中,TodoList类不需要知道具体有哪些ViewController会关注它的变化,只需要发布通知即可。
  2. 易于扩展:当有新的观察者需要监听主题的变化时,只需要注册为观察者即可,不需要修改主题的代码。比如在主题颜色更新场景中,新添加一个SettingsViewController也需要监听主题颜色变化,只需要在SettingsViewController中注册为ThemeColorChanged通知的观察者即可,ThemeManager类无需改动。
  3. 支持广播通信:一个主题状态变化可以通知多个观察者,实现一对多的关系。就像在网络请求状态通知场景中,多个视图控制器都可以监听NetworkManager的网络请求状态变化通知。

缺点

  1. 内存管理问题:如果不及时移除观察者,可能会导致内存泄漏。例如在使用NSNotificationCenter时,如果在ViewController销毁时没有移除对特定通知的观察,ViewController虽然被释放,但通知中心仍然持有对它的引用,导致内存无法回收。
  2. 调试困难:由于观察者模式采用了异步通信的方式,特别是在复杂的应用中,很难追踪通知的传递路径和处理逻辑。比如在一个大型项目中,多个地方都发布了相同名称的通知,很难快速定位是哪个地方发布的通知导致了某个观察者的异常行为。
  3. 性能问题:当有大量的观察者时,发布通知或KVO通知触发可能会带来性能开销。因为需要遍历所有的观察者并执行相应的处理方法,这在性能敏感的场景下可能会影响应用的响应速度。

优化与最佳实践

  1. 及时移除观察者:在对象销毁时,一定要记得移除对相关通知或KVO观察的注册。可以在dealloc方法中统一进行移除操作,例如在前面的各个示例中,我们都在dealloc方法中通过NSNotificationCenterremoveObserver:name:object:方法或者KVO的removeObserver:forKeyPath:方法移除了观察者。
  2. 使用唯一的通知名称:为了避免调试困难,在应用中尽量使用唯一的通知名称。可以采用命名空间的方式,例如在应用名称后加上具体的功能模块和通知类型,如com.example.app.TodoListChanged,这样可以减少通知名称冲突的可能性。
  3. 批量处理通知:在性能敏感的场景下,可以考虑批量处理通知。例如,在短时间内可能会多次触发某个主题的状态变化通知,我们可以设置一个定时器,在定时器触发时统一处理这些通知,而不是每次通知触发都立即处理,这样可以减少不必要的性能开销。

通过深入理解和合理运用观察者模式,结合Objective-C提供的NSNotificationCenter和KVO机制,开发者可以构建出更加灵活、可扩展且易于维护的iOS应用。在实际开发中,根据不同的场景和需求,选择合适的实现方式,并遵循最佳实践原则,能够有效地提高代码质量和开发效率。