iOS开发基础21-对象间通信:通知、代理、KVO 与 Block 全解析

iOS 对象间通信:通知、代理、KVO 与 Block 全解析

iOS 开发中对象间通信有四种核心方式:通知(Notification)、代理(Delegate)、键值观察(KVO)和 Block。本文详细讲解每种方式的用法、底层机制、适用场景,并通过购物车案例对比实现。


一、通知(Notification)

1. 通知中心与通知结构

每个应用有一个默认通知中心 [NSNotificationCenter defaultCenter],负责通知的发布与分发。通知是同步发送的——postNotification 会阻塞当前线程,直到所有监听者的方法执行完毕。

NSNotification 包含三个属性:

属性 类型 说明
name NSString 通知名称,唯一标识
object id 通知发布者(通常为 self
userInfo NSDictionary 附带的额外数据
NSNotification *note = [NSNotification notificationWithName:@"testNotification" object:self userInfo:@{@"key": @"value"}];

2. 发布通知

// 方式1:发布已构造的通知对象
[[NSNotificationCenter defaultCenter] postNotification:note];

// 方式2:仅名称和发布者
[[NSNotificationCenter defaultCenter] postNotificationName:@"testNotification" object:self];

// 方式3:名称、发布者、额外数据(最常用)
[[NSNotificationCenter defaultCenter] postNotificationName:@"testNotification" object:self userInfo:@{@"key": @"value"}];

3. 注册监听者

selector 方式

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(handleNotification:)
                                             name:@"testNotification"
                                           object:nil];

- (void)handleNotification:(NSNotification *)note {
    NSLog(@"userInfo: %@", note.userInfo);
}

Block 方式

// Block 方式返回一个观察者对象,必须保存并在 dealloc 中移除它
self.notificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:@"testNotification"
                                                                               object:nil
                                                                                queue:[NSOperationQueue mainQueue]
                                                                           usingBlock:^(NSNotification * _Nonnull note) {
    NSLog(@"Received: %@", note.userInfo);
}];

关键:Block 方式的返回值是一个匿名观察者对象,移除时要移除这个返回值,而不是 self

4. 移除监听者

- (void)dealloc {
    // selector 方式:移除 self
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    // Block 方式:移除返回的观察者对象
    if (self.notificationObserver) {
        [[NSNotificationCenter defaultCenter] removeObserver:self.notificationObserver];
    }
}

iOS 9+ 变化:使用 addObserver:selector:name:object: 注册时,系统对观察者使用 weak 引用,对象释放后自动移除,无需手动在 dealloc 中移除。但 Block 方式注册的观察者仍需手动移除。

5. 常用系统通知

设备方向通知

// 必须先开始生成方向通知,否则收不到
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(orientationChanged:)
                                             name:UIDeviceOrientationDidChangeNotification
                                           object:nil];

键盘通知

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillShow:)
                                             name:UIKeyboardWillShowNotification
                                           object:nil];

- (void)keyboardWillShow:(NSNotification *)note {
    // 键盘最终 frame(相对于屏幕)
    CGRect keyboardFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    // 动画时长
    NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    // 动画曲线
    UIViewAnimationOptions curve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16;
    
    [UIView animateWithDuration:duration delay:0 options:curve animations:^{
        // 适配键盘,调整视图 frame 或约束
    } completion:nil];
}

常用键盘通知:UIKeyboardWillShowNotificationUIKeyboardDidShowNotificationUIKeyboardWillHideNotificationUIKeyboardDidHideNotification


二、代理(Delegate)

1. 代理模式

代理是一对一的通信方式,一个对象将某些行为委托给另一个对象处理。只能有一个 delegate,因此是严格的一对一关系。

2. 使用步骤

定义协议

// XMGWineCellDelegate.h
@class XMGWineCell;

@protocol XMGWineCellDelegate <NSObject>

@required // 必须实现(默认)
- (void)wineCellDidClickPlusButton:(XMGWineCell *)cell;

@optional // 可选实现
- (void)wineCellDidClickMinusButton:(XMGWineCell *)cell;

@end

声明 delegate 属性

@interface XMGWineCell : UITableViewCell

// delegate 必须用 weak,避免循环引用
@property (nonatomic, weak) id<XMGWineCellDelegate> delegate;

@end

调用代理方法

@implementation XMGWineCell

- (void)plusButtonClicked {
    // 可选方法调用前必须检查 respondsToSelector:
    if ([self.delegate respondsToSelector:@selector(wineCellDidClickPlusButton:)]) {
        [self.delegate wineCellDidClickPlusButton:self];
    }
}

- (void)minusButtonClicked {
    if ([self.delegate respondsToSelector:@selector(wineCellDidClickMinusButton:)]) {
        [self.delegate wineCellDidClickMinusButton:self];
    }
}

@end

控制器实现代理

@interface ViewController () <XMGWineCellDelegate>
@end

@implementation ViewController

- (void)wineCellDidClickPlusButton:(XMGWineCell *)cell {
    NSLog(@"加号点击: %@", cell.wine.name);
}

- (void)wineCellDidClickMinusButton:(XMGWineCell *)cell {
    NSLog(@"减号点击: %@", cell.wine.name);
}

@end

关键delegate 属性必须用 weak(或 assign),否则会造成循环引用(控制器强引用 Cell,Cell 强引用 delegate=控制器)。


三、键值观察(KVO)

1. KVO 机制

KVO 用于监听对象属性的变化。底层通过 ISA-Swizzling 实现:注册观察时,Runtime 动态创建一个被观察类的子类(如 NSKVONotifying_Wine),重写被观察属性的 setter 方法,在 setter 中调用 willChangeValueForKey:didChangeValueForKey: 触发通知。

前提条件:被观察的属性必须通过 setter 方法(或 KVC setValue:forKey:)修改,直接修改实例变量(_count = 1)不会触发 KVO。

2. 添加监听

[wine addObserver:self
        forKeyPath:@"count"
           options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
           context:nil];
options 说明
NSKeyValueObservingOptionNew change 字典包含新值
NSKeyValueObservingOptionOld change 字典包含旧值
NSKeyValueObservingOptionInitial 注册时立即触发一次回调
NSKeyValueObservingOptionPrior 值改变前触发一次回调

3. 实现回调

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"count"]) {
        NSNumber *newValue = change[NSKeyValueChangeNewKey];
        NSNumber *oldValue = change[NSKeyValueChangeOldKey];
        NSLog(@"count: %@ -> %@", oldValue, newValue);
    } else {
        // 未识别的 keyPath 必须调用 super,避免父类的 KVO 被忽略
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

4. 移除监听

- (void)dealloc {
    // 移除未注册的监听会崩溃,需确保只移除已注册的
    [wine removeObserver:self forKeyPath:@"count"];
}

5. iOS 11+ Block-based KVO

iOS 11 引入了基于 Block 的 KVO,返回 NSKeyValueObservation 对象,自动管理生命周期,无需手动移除:

@property (nonatomic, strong) NSKeyValueObservation *countObservation;

self.countObservation = [wine observeValueForKeyPath:@"count"
                                              options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                                          changeHandler:^(Wine * _Nonnull obj, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
    NSLog(@"count changed: %@", change[NSKeyValueChangeNewKey]);
}];

四、Block

1. Block 基础

Block 是封装了代码和捕获变量的对象,类似函数指针但能捕获上下文变量。

// 声明 Block 类型
typedef void(^ExampleBlock)(NSString *message);

// 创建并调用
ExampleBlock block = ^(NSString *message) {
    NSLog(@"Message: %@", message);
};
block(@"Hello, Block!");

2. Block 的三种类型

类型 存储位置 触发条件
NSGlobalBlock 全局区(类似单例) 未捕获外部变量
NSStackBlock 栈上 捕获了外部变量(MRC 下)
NSMallocBlock 堆上 对 StackBlock 执行 copy 后

ARC 下,Block 作为属性或被强引用时会自动 copy 到堆上,通常无需手动管理。但属性仍习惯用 copy 修饰。

3. Block 作为回调

// 声明
@interface CustomClass : NSObject

// Block 属性用 copy(ARC 下 strong 也可,但 copy 更明确)
@property (nonatomic, copy) void (^completionHandler)(BOOL success);

- (void)performActionWithCompletion:(void (^)(BOOL success))completion;

@end

@implementation CustomClass

- (void)performActionWithCompletion:(void (^)(BOOL success))completion {
    self.completionHandler = completion;
    // 执行操作...
    if (self.completionHandler) {
        self.completionHandler(YES);
    }
}

@end

4. 循环引用与弱引用

Block 会强引用捕获的对象,如果对象又强引用 Block,就会形成循环引用。解决方案是 Weak-Strong Dance

__weak typeof(self) weakSelf = self;
self.completionHandler = ^(BOOL success) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return; // self 已释放,直接返回
    // 使用 strongSelf 代替 self,避免循环引用
    strongSelf.label.text = success ? @"成功" : @"失败";
};

__weak 打破循环引用;__strong 在 Block 执行期间持有 self,防止执行过程中 self 被释放。


五、案例:购物车总价计算

以购物车 Cell 中加减按钮点击后更新总价为例,对比四种通信方式。

1. 通知方式

// Cell 中发布通知
- (void)plusButtonClicked {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"plusClickNotification" object:self];
}

// 控制器中注册监听
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(plusClick:)
                                                 name:@"plusClickNotification"
                                               object:nil];
}

- (void)plusClick:(NSNotification *)note {
    XMGWineCell *cell = note.object;
    NSInteger total = self.totalPriceLabel.text.integerValue + cell.wine.money.integerValue;
    self.totalPriceLabel.text = [NSString stringWithFormat:@"%ld", (long)total];
}

2. 代理方式

// Cell 中调用代理
- (void)plusButtonClicked {
    if ([self.delegate respondsToSelector:@selector(wineCellDidClickPlusButton:)]) {
        [self.delegate wineCellDidClickPlusButton:self];
    }
}

// 控制器实现代理
- (void)wineCellDidClickPlusButton:(XMGWineCell *)cell {
    NSInteger total = self.totalPriceLabel.text.integerValue + cell.wine.money.integerValue;
    self.totalPriceLabel.text = [NSString stringWithFormat:@"%ld", (long)total];
}

3. KVO 方式

// Wine 模型中 count 属性变化时触发 KVO
// 控制器中为每个 wine 添加监听
- (void)configureCell:(XMGWineCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    Wine *wine = self.wineArray[indexPath.row];
    [wine addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(Wine *)wine change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"count"]) {
        NSInteger newCount = [change[NSKeyValueChangeNewKey] integerValue];
        NSInteger oldCount = [change[NSKeyValueChangeOldKey] integerValue];
        NSInteger delta = (newCount - oldCount) * wine.money.integerValue;
        NSInteger total = self.totalPriceLabel.text.integerValue + delta;
        self.totalPriceLabel.text = [NSString stringWithFormat:@"%ld", (long)total];
    }
}

KVO 监听的是数据模型的属性变化,而非按钮点击。Cell 中点击按钮后修改 wine.count,KVO 自动触发。

4. Block 方式

// Cell 中声明 Block
typedef void(^WineCellActionBlock)(XMGWineCell *cell);

@interface XMGWineCell : UITableViewCell
@property (nonatomic, copy) WineCellActionBlock plusActionBlock;
@property (nonatomic, copy) WineCellActionBlock minusActionBlock;
@end

// Cell 中调用
- (void)plusButtonClicked {
    if (self.plusActionBlock) {
        self.plusActionBlock(self);
    }
}

// 控制器中设置回调(注意 __weak 避免循环引用)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    XMGWineCell *cell = [XMGWineCell cellWithTableView:tableView];
    __weak typeof(self) weakSelf = self;
    
    [cell setPlusActionBlock:^(XMGWineCell *cell) {
        NSInteger total = weakSelf.totalPriceLabel.text.integerValue + cell.wine.money.integerValue;
        weakSelf.totalPriceLabel.text = [NSString stringWithFormat:@"%ld", (long)total];
    }];
    
    [cell setMinusActionBlock:^(XMGWineCell *cell) {
        NSInteger total = weakSelf.totalPriceLabel.text.integerValue - cell.wine.money.integerValue;
        weakSelf.totalPriceLabel.text = [NSString stringWithFormat:@"%ld", (long)total];
    }];
    
    return cell;
}

六、Swift 版本对照

通知

// 注册
NotificationCenter.default.addObserver(self, selector: #selector(handleNote(_:)), name: NSNotification.Name("testNote"), object: nil)

// 发布
NotificationCenter.default.post(name: NSNotification.Name("testNote"), object: nil, userInfo: ["key": "value"])

// Block 方式
let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name("testNote"), object: nil, queue: .main) { note in
    print(note.userInfo ?? [:])
}
// deinit 中移除
NotificationCenter.default.removeObserver(observer)

代理

protocol WineCellDelegate: AnyObject { // class-only protocol,才能用 weak
    func wineCellDidClickPlusButton(_ cell: WineCell)
}

weak var delegate: WineCellDelegate?

// 调用
delegate?.wineCellDidClickPlusButton(self)

KVO

class Wine: NSObject {
    @objc dynamic var count: Int = 0 // 必须 @objc dynamic
}

var observation: NSKeyValueObservation?

observation = wine.observe(\.count, options: [.new, .old]) { wine, change in
    print("count: \(change.oldValue) -> \(change.newValue)")
}
// observation 释放时自动移除监听

Block/闭包

var completionHandler: ((Bool) -> Void)?

// 弱引用
completionHandler = { [weak self] success in
    guard let self = self else { return }
    self.label.text = success ? "成功" : "失败"
}

七、四种方式对比

维度 通知 代理 KVO Block
通信关系 一对多 / 多对多 一对一 一对多(多个观察者) 一对一
耦合度 极低(通过名称解耦) 中(需遵守协议) 低(监听属性) 低(代码内联)
返回值 支持 支持
多参数 通过 userInfo 字典 方法参数直接传递 change 字典 闭包参数
调试难度 高(通知名称全局搜索) 低(协议方法明确) 中(keyPath 字符串) 中(Block 内联)
循环引用风险 无(weak delegate) 高(需 weak-strong)
典型场景 跨层级广播、系统事件 Cell→控制器交互 数据模型属性变化 异步回调、简单交互

选择建议

  • 跨多个对象广播(如登录状态变化、键盘弹出)→ 通知
  • Cell 与控制器交互、有明确协议规范 → 代理
  • 监听数据模型属性变化(如 count、status)→ KVO
  • 简单异步回调、代码内联更清晰 → Block

八、总结

  • 通知:一对多广播,解耦性最强,但调试困难,注意通知名称唯一性和移除观察者。
  • 代理:一对一通信,规范清晰,delegate 必须用 weak,可选方法调用前检查 respondsToSelector:
  • KVO:监听属性变化,底层 ISA-Swizzling,要求通过 setter 修改属性;iOS 11+ 推荐 Block-based KVO 自动管理生命周期。
  • Block:代码内联简洁,能捕获变量,但需注意循环引用,使用 Weak-Strong Dance 解决。
  • 四种方式各有适用场景,实际开发中常组合使用(如 Cell 用代理或 Block,全局状态用通知,数据变化用 KVO)。

posted @ 2015-07-26 01:25  Mr.陳  阅读(612)  评论(0)    收藏  举报