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];
}
常用键盘通知:
UIKeyboardWillShowNotification、UIKeyboardDidShowNotification、UIKeyboardWillHideNotification、UIKeyboardDidHideNotification。
二、代理(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)。

浙公网安备 33010602011771号