iOS 中的 Subscription 与 RACDisposable:订阅生命周期管理

在 iOS 开发中,很多功能本质上都属于"订阅":

  • 监听业务状态变化

  • 监听 WebSocket 或 IM 消息

  • 监听 NSNotificationCenter 通知

  • 监听 KVO 属性变化

  • 监听定时器

  • 监听网络请求进度或结果

  • 监听键盘、前后台、网络可达性状态

  • 订阅 ReactiveObjC 的 RACSignal

这些场景都有一个共同问题:

当页面、组件或业务对象不再需要接收消息时,如何可靠地取消监听并释放关联资源?

SubscriptionRACDisposable 解决的就是这个问题。

一、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 应满足两个要求:

  1. 幂等:可重复调用,不会重复清理资源。

  2. 可释放:调用后不再接收后续事件。

二、为什么订阅关系需要单独管理

很多早期代码会直接注册监听:

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

RACSignalRACSubject

订阅回调

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

接收 nexterrorcompleted 事件

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 的具体实现

取消方法

常见为 invalidatecancel

dispose

使用范围

任意观察者、事件、任务

RAC Signal 订阅和相关资源

事件模型

由业务自行定义

next/error/completed

组合能力

需自行实现

RACCompoundDisposable

操作符支持

mapfiltertakeUntil

调度支持

需自行实现

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 复用、组件隐藏、账号切换等场景,应主动取消不再需要的订阅。

  • 状态类事件建议让新订阅者立即获得当前状态;纯事件类消息通常只消费订阅后的新事件。

十二、结论

SubscriptionRACDisposable 的核心价值不在于回调本身,而在于将订阅关系显式建模为可管理资源。

rust 复制代码
创建订阅
  -> 获得 Token / Disposable
  -> 由需要接收事件的一方持有
  -> 在生命周期结束或业务切换时取消
  -> 自动解除回调、监听、任务和资源占用

对于未使用 ReactiveObjC 的工程,可以自定义轻量 Subscription,通过 invalidate 管理观察者生命周期。

对于使用 ReactiveObjC 的工程,应将 RACDisposable 视为订阅令牌,并结合:

  • RACCompoundDisposable

  • takeUntil:

  • rac_willDeallocSignal

  • deliverOnMainThread

  • weakify/strongify

管理复杂页面和业务组件的事件流。

在直播间、聊天、播放器、实时协作、动态表单等多状态源场景中,订阅生命周期管理是稳定性和可维护性的基础能力。

相关推荐
代码的小搬运工3 小时前
【iOS】3G-Share仿写总结
macos·ios·cocoa
2501_916007475 小时前
iOS和macOS应用程序性能分析和优化工具使用综合指南
android·macos·ios·小程序·uni-app·iphone·webview
游戏开发爱好者86 小时前
iOS开发IDE有哪些 Xcode 和 快蝎 轻量替代方案
ide·vscode·ios·个人开发·xcode·swift·敏捷流程
ZZH_AI项目交付1 天前
同一套前摄心率算法,为什么 iPhone 16 Pro 稳,iPhone XR 会失准?
ios·app·ai编程
DeMinds1 天前
内容没有丢,我为什么总在重新整理?|DeMinds 如何让工作接着继续
ios·github·markdown
apd_csdn1 天前
清华邮箱苹果邮件app设置(全)
ios·thu
MDM.Plus1 天前
从“遥控”到“自治”:苹果 MDM 技术的代际跨越与业务重构
ios·智能手机·重构·mdm
黑化旺仔1 天前
iOS - 天气预报仿写总结
ios