在 iOS 开发中,很多功能本质上都属于"订阅":
-
监听业务状态变化
-
监听 WebSocket 或 IM 消息
-
监听
NSNotificationCenter通知 -
监听 KVO 属性变化
-
监听定时器
-
监听网络请求进度或结果
-
监听键盘、前后台、网络可达性状态
-
订阅 ReactiveObjC 的
RACSignal
这些场景都有一个共同问题:
当页面、组件或业务对象不再需要接收消息时,如何可靠地取消监听并释放关联资源?
Subscription 和 RACDisposable 解决的就是这个问题。
一、Subscription 是什么
Subscription 不是 iOS 或 Objective-C 的固定系统类型,而是一种通用设计模式。
它代表一次订阅关系的"凭证"或"令牌":
rust
订阅方
-> 创建订阅
-> 获得 Subscription Token
-> 持有 Token
-> 需要时取消 Token
例如:
ini
Subscription *subscription =
[eventCenter subscribeEvent:@"LiveRoomDidEnd"
handler:^(NSDictionary *payload) {
NSLog(@"Live room ended: %@", payload);
}];
之后可以显式取消:
ini
[subscription invalidate];
或者让持有它的对象释放,借助 dealloc 自动取消。
一个典型的 Subscription 接口通常如下:
less
@interface Subscription : NSObject
- (void)invalidate;
@end
这里的 invalidate 应满足两个要求:
-
幂等:可重复调用,不会重复清理资源。
-
可释放:调用后不再接收后续事件。
二、为什么订阅关系需要单独管理
很多早期代码会直接注册监听:
objectivec
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(roomDidEnd:)
name:@"LiveRoomDidEnd"
object:nil];
然后在合适的时机移除:
css
[[NSNotificationCenter defaultCenter] removeObserver:self];
这种方式的问题是,注册和注销很容易分散在不同位置:
csharp
注册发生在 init
注销可能忘在 dealloc
页面复用时可能需要在 viewDidDisappear 注销
房间切换时可能需要提前注销
一旦漏掉注销,常见后果包括:
-
已销毁或已离开页面的组件仍收到状态更新
-
旧直播间的消息驱动新直播间 UI
-
定时器、通知、KVO、WebSocket 回调长期存活
-
block 强引用
self,导致内存泄漏 -
多次进入页面后重复订阅,出现重复刷新或重复请求
Subscription Token 将"取消订阅"变成一项明确资源:
订阅关系不是隐式副作用,
而是一个需要持有、取消、释放的对象。
三、通用 Subscription 的结构
一个简化的事件中心可以这样实现:
objectivec
typedef void (^EventHandler)(id _Nullable payload);
@interface Subscription : NSObject
- (void)invalidate;
@end
@interface EventCenter : NSObject
- (Subscription *)subscribeEvent:(NSString *)eventName
handler:(EventHandler)handler;
- (void)publishEvent:(NSString *)eventName
payload:(id _Nullable)payload;
@end
内部关系可以理解为:
markdown
EventCenter
└── eventName
└── subscriptionID
└── handler block
Subscription
├── eventName
├── subscriptionID
└── invalidate 时通知 EventCenter 删除对应 handler
伪代码如下:
ini
- (Subscription *)subscribeEvent:(NSString *)eventName
handler:(EventHandler)handler {
NSUUID *identifier = [NSUUID UUID];
self.handlers[eventName][identifier] = [handler copy];
Subscription *subscription = [[Subscription alloc] init];
subscription.eventName = eventName;
subscription.identifier = identifier;
subscription.eventCenter = self;
return subscription;
}
取消时:
objectivec
- (void)invalidate {
if (self.invalidated) {
return;
}
self.invalidated = YES;
[self.eventCenter removeHandlerForEvent:self.eventName
identifier:self.identifier];
}
订阅方只需要持有返回值:
objectivec
@property (nonatomic, strong) Subscription *roomEndSubscription;
组件释放时,Subscription 也会随之释放;如果其 dealloc 调用 invalidate,订阅关系便会自动清理。
四、RACDisposable 是 ReactiveObjC 中的 Subscription
在 ReactiveObjC 中,订阅 RACSignal 后获得的对象叫做 RACDisposable:
ini
RACDisposable *disposable =
[signal subscribeNext:^(id value) {
NSLog(@"Receive value: %@", value);
}];
它本质上就是 RAC 对 Subscription Token 的实现。
两者可以这样对应:
通用概念
ReactiveObjC
Subscription
RACDisposable
invalidate
dispose
发布者/EventCenter
RACSignal、RACSubject
订阅回调
subscribeNext: 的 block
取消订阅
[disposable dispose]
多个订阅聚合
RACCompoundDisposable
取消订阅:
ini
[disposable dispose];
dispose 的含义不是单纯"把 block 置空",而是释放这次订阅产生的资源,例如:
-
从
RACSubject的订阅者列表中移除 -
移除通知监听
-
结束 KVO 监听
-
停止定时器
-
取消网络请求
-
取消内部子信号订阅
-
释放与该事件流相关的资源
五、RAC 的订阅过程
一个 RACSignal 可以这样创建:
ini
RACSignal *signal = [RACSignal createSignal:
^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"Hello"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
NSLog(@"Subscription disposed");
}];
}];
订阅:
ini
RACDisposable *disposable =
[signal subscribeNext:^(NSString *value) {
NSLog(@"%@", value);
}];
其核心过程可以理解为:
lua
调用 subscribeNext:
↓
RAC 创建 Subscriber
↓
Subscriber 持有 next/error/completed 回调
↓
Signal 执行自身的订阅逻辑
↓
Signal 返回 RACDisposable
↓
调用方持有 Disposable
事件传递方向:
lua
Signal
-> Subscriber
-> next / error / completed block
资源释放方向:
rust
调用方
-> RACDisposable dispose
-> 取消上游工作、解除订阅关系
六、RACDisposable 的核心特性
1. 可取消
ini
RACDisposable *disposable =
[signal subscribeNext:^(id value) {
NSLog(@"%@", value);
}];
[disposable dispose];
调用 dispose 后,订阅者不应再收到后续事件。
2. 幂等
dispose 应可重复调用:
ini
[disposable dispose];
[disposable dispose];
[disposable dispose];
资源释放逻辑只执行一次。
简化实现如下:
objectivec
- (void)dispose {
void (^disposeBlock)(void) = nil;
@synchronized (self) {
disposeBlock = self.disposeBlock;
self.disposeBlock = nil;
}
if (disposeBlock != nil) {
disposeBlock();
}
}
第一次调用取出并执行 disposeBlock,之后 block 已经被置空,不会发生重复清理。
3. 可组合
一个页面通常同时存在多个订阅:
ini
RACDisposable *chatDisposable;
RACDisposable *micSeatDisposable;
RACDisposable *roomStateDisposable;
RACDisposable *keyboardDisposable;
RAC 提供 RACCompoundDisposable 统一管理:
ini
RACCompoundDisposable *disposables =
[RACCompoundDisposable compoundDisposable];
[disposables addDisposable:chatDisposable];
[disposables addDisposable:micSeatDisposable];
[disposables addDisposable:roomStateDisposable];
[disposables addDisposable:keyboardDisposable];
离开页面或直播间时:
ini
[disposables dispose];
效果相当于依次调用:
ini
[chatDisposable dispose];
[micSeatDisposable dispose];
[roomStateDisposable dispose];
[keyboardDisposable dispose];
这非常适合直播间、播放器、聊天页、复杂表单页等多状态源场景。
4. 自动跟随生命周期
对于 UI 订阅,常见写法是:
python
@weakify(self);
[[[self.roomStore.chatStateSignal
deliverOnMainThread]
takeUntil:self.rac_willDeallocSignal]
subscribeNext:^(LiveChatState *state) {
@strongify(self);
[self renderChatState:state];
}];
含义是:
rust
聊天室状态变化
-> 更新 UI
ViewController 即将释放
-> takeUntil 终止订阅
-> 关联 Disposable 被 dispose
-> 不再接收状态变化
takeUntil: 能避免长期 Signal 持续向已经释放的 UI 组件发送事件。
七、RACSignal、RACSubscriber、RACDisposable 的关系
RAC 的核心对象职责可以总结为:
对象
职责
RACSignal
定义事件流和订阅逻辑
RACSubscriber
接收 next、error、completed 事件
RACDisposable
取消订阅、释放订阅相关资源
RACCompoundDisposable
组合管理多个 Disposable
RACSubject
既能被订阅,也能主动发送事件
RACReplaySubject
可缓存历史事件并回放给新订阅者
关系如下:
shell
RACSignal
-> subscribe
-> RACSubscriber
-> receive next/error/completed
subscribe 的返回值
-> RACDisposable
-> dispose 后解除订阅并回收资源
八、冷信号与热信号
理解 RACDisposable 时,需要区分冷信号和热信号。
冷信号
冷信号通常会在每次订阅时创建一份新的工作。
ini
RACSignal *requestSignal = [RACSignal createSignal:
^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"Start network request");
return nil;
}];
两次订阅:
ini
[requestSignal subscribeNext:nil];
[requestSignal subscribeNext:nil];
通常会触发两次请求:
sql
Start network request
Start network request
适合:
-
网络请求
-
文件读取
-
数据库查询
-
一次性异步任务
-
延迟计算
dispose 在冷信号中的常见职责是取消当前这一次任务。
热信号
热信号代表一个已经存在的事件源,多个订阅者共享事件。
ini
RACSubject *messageSubject = [RACSubject subject];
例如 WebSocket 消息:
ini
[messageSubject sendNext:message];
当前所有订阅者都会收到消息:
rust
WebSocket / IM Message
-> RACSubject sendNext:
-> ChatComponent
-> GiftComponent
-> AudienceComponent
适合:
-
WebSocket 或 IM 消息流
-
App 生命周期
-
用户点击事件
-
键盘事件
-
网络状态变化
-
房间状态变化
dispose 在热信号中的常见职责是将当前订阅者从订阅列表中移除。
九、典型案例
1. 通知订阅
将系统通知包装为 Signal:
ini
RACSignal *backgroundSignal =
[[NSNotificationCenter defaultCenter]
rac_addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil];
订阅:
ini
RACDisposable *disposable =
[backgroundSignal subscribeNext:^(NSNotification *notification) {
NSLog(@"App entered background");
}];
取消订阅:
ini
[disposable dispose];
dispose 后,RAC 会解除对应的通知监听。
2. KVO 属性监听
objectivec
RACDisposable *disposable =
[[self.player rac_valuesForKeyPath:@"status"
observer:nil]
subscribeNext:^(NSNumber *status) {
NSLog(@"Player status: %@", status);
}];
取消后,RAC 清理底层 KVO 关系。
对于 KVO,这种资源管理尤其重要,因为传统 KVO 的注册、移除和对象生命周期很容易出错。
3. 定时器
ini
RACSignal *timerSignal =
[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]];
RACDisposable *disposable =
[timerSignal subscribeNext:^(NSDate *date) {
NSLog(@"Timer tick: %@", date);
}];
停止定时器:
ini
[disposable dispose];
在直播间中,这可用于:
-
PK 倒计时
-
禁言剩余时间
-
礼物面板倒计时
-
商品讲解倒计时
-
房间心跳显示
4. 网络请求
ini
RACSignal *requestSignal = [self requestRoomSnapshotSignal];
RACDisposable *disposable =
[requestSignal subscribeNext:^(LiveRoomSnapshot *snapshot) {
[self renderRoomSnapshot:snapshot];
} error:^(NSError *error) {
[self showRequestError:error];
}];
当用户快速离开房间时:
ini
[disposable dispose];
理想情况下,底层网络层应响应取消逻辑,例如取消 NSURLSessionTask:
ini
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
5. 直播间状态订阅
ini
@weakify(self);
RACDisposable *micSeatDisposable =
[[[self.roomStore.micSeatStateSignal
distinctUntilChanged]
deliverOnMainThread]
subscribeNext:^(MicSeatState *state) {
@strongify(self);
[self.micSeatComponent renderState:state];
}];
离开房间:
ini
[micSeatDisposable dispose];
这里的职责边界是:
rust
RoomStore
-> 维护麦位真实状态
-> 发布 MicSeatState
MicSeatComponent
-> 只订阅并渲染状态
RACDisposable
-> 管理这一条订阅关系的生命周期
6. 页面级统一取消
ini
@interface LiveRoomViewController ()
@property (nonatomic, strong) RACCompoundDisposable *roomDisposables;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.roomDisposables =
[RACCompoundDisposable compoundDisposable];
RACDisposable *chatDisposable =
[self.roomStore.chatStateSignal subscribeNext:^(id state) {
// Render chat.
}];
RACDisposable *giftDisposable =
[self.roomStore.giftStateSignal subscribeNext:^(id state) {
// Render gift panel.
}];
[self.roomDisposables addDisposable:chatDisposable];
[self.roomDisposables addDisposable:giftDisposable];
}
- (void)leaveRoom {
[self.roomDisposables dispose];
self.roomDisposables = nil;
}
这种模式非常适合直播间切换:
rust
离开旧房间
-> dispose 所有旧房间订阅
-> 断开旧 IM / RTC / Store 监听
进入新房间
-> 创建新的 Disposable 集合
-> 建立新房间订阅
十、Subscription 与 RACDisposable 的差异
两者本质相同,但抽象层次不同。
对比项
Subscription
RACDisposable
定义
通用设计模式
ReactiveObjC 的具体实现
取消方法
常见为 invalidate、cancel
dispose
使用范围
任意观察者、事件、任务
RAC Signal 订阅和相关资源
事件模型
由业务自行定义
next/error/completed
组合能力
需自行实现
RACCompoundDisposable
操作符支持
无
map、filter、takeUntil 等
调度支持
需自行实现
RACScheduler
适合场景
轻量业务架构
已采用 RAC 的响应式工程
可以把它们理解为:
Subscription 是模式。
RACDisposable 是该模式在 ReactiveObjC 中的标准实现。
十一、内存管理注意事项
订阅回调最常见的问题是循环引用。
错误写法:
ini
self.disposable =
[self.signal subscribeNext:^(id value) {
[self renderValue:value];
}];
可能形成:
rust
self
-> disposable
-> signal / subscriber
-> callback block
-> self
更安全的写法:
python
@weakify(self);
self.disposable =
[self.signal subscribeNext:^(id value) {
@strongify(self);
if (self == nil) {
return;
}
[self renderValue:value];
}];
没有使用 RAC 宏时:
objectivec
__weak typeof(self) weakSelf = self;
self.disposable =
[self.signal subscribeNext:^(id value) {
__strong typeof(weakSelf) self = weakSelf;
if (self == nil) {
return;
}
[self renderValue:value];
}];
还需要注意:
-
无限事件流必须考虑何时
dispose。 -
UI 更新应通过
deliverOnMainThread或等效机制切换到主线程。 -
不要依赖"对象迟早会释放"来清理长生命周期订阅。
-
房间切换、Cell 复用、组件隐藏、账号切换等场景,应主动取消不再需要的订阅。
-
状态类事件建议让新订阅者立即获得当前状态;纯事件类消息通常只消费订阅后的新事件。
十二、结论
Subscription 和 RACDisposable 的核心价值不在于回调本身,而在于将订阅关系显式建模为可管理资源。
rust
创建订阅
-> 获得 Token / Disposable
-> 由需要接收事件的一方持有
-> 在生命周期结束或业务切换时取消
-> 自动解除回调、监听、任务和资源占用
对于未使用 ReactiveObjC 的工程,可以自定义轻量 Subscription,通过 invalidate 管理观察者生命周期。
对于使用 ReactiveObjC 的工程,应将 RACDisposable 视为订阅令牌,并结合:
-
RACCompoundDisposable -
takeUntil: -
rac_willDeallocSignal -
deliverOnMainThread -
weakify/strongify
管理复杂页面和业务组件的事件流。
在直播间、聊天、播放器、实时协作、动态表单等多状态源场景中,订阅生命周期管理是稳定性和可维护性的基础能力。