ReactiveCocoa组件的内存逻辑分析

为了更好了解ReactiveCocoa,以便在开发过程中更准确的使用,曾花蛮多时间阅读ReactiveCocoa的实现源码。漫无目的地阅读源码是难以坚持的,因此总习惯找一条脉络,分析ReactiveCocoa的内存管理逻辑显然是一条不错的路线。

ReactiveCocoa的三大基础组件(signal、subscriber、disposable)中,signal的逻辑是最复杂的;而众多signal中,RACDynamicSignal的实现逻辑最为晦涩难懂,它的使用频率也最高,因此本文选择对围绕RACDynamicSignal的信号处理逻辑进行内存分析,过程中自然也会将subscriber、disposable的逻辑也给弄清楚;站在另外一个角度来看,倘若能将RACDynamicSignal的内存逻辑分析清除,分析RACSubject等自然也不在话下。

本文是我围绕内存管理,分析源码过程中的一些记录。

冷信号和订阅者

关于冷信号和订阅者,有些事实是我之前不太清楚的,直接罗列如下:

  • 冷信号(譬如RACDynamicSignal)和订阅者(RACSubscriber)之间没有持有(引用)关系
  • RACSubscriber一旦接收到sendError:或者sendCompleted消息,就会把3个block设置为nil

下图是更生动的描述:

RACDynamicSignal-RACSubscriber@2x.png

看一段代码:

{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
// timing 5
[subscriber sendNext:@"1"];
[subscriber sendNext:@"2"];
[subscriber sendCompleted];
// timing 6
}];
return nil;
// timing 3
}];
// timing 1
[signal subscribeNext:^(id x) {
NSLog(@"next: %@", x);
} error:^(NSError *error) {
NSLog(@"error: %@", error);
} completed:^{
NSLog(@"completed");
}];
// timing 2
}
// timing 4

问题:在timing 5处,signal对象还存在吗?

答案:不存在!signal没有被任何对象持有,到timing 4处,它就被释放掉了。

热信号和订阅者

和冷信号不同,热信号和订阅者之间是有持有关系的,即:

  • 热信号(譬如RACSubject)会持有订阅者

下图是更生动的描述:

RACSubject-RACSubscriber@2x.png

RACSubject#subscribers是一个数组,每次被订阅时,它就将订阅者存到该数组中,以便日后产生事件时向它们发送sendNext:/sendError:/sendCompleted消息。

真·生命周期

上面分析的生命周期其实将一些内部细节给过滤掉了。

先来看段代码:

{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[RACScheduler.mainThreadScheduler afterDelay:2 schedule:^{
// async block 2
[subscriber sendNext:@2];
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
NSLog(@"dispose in didSubscribe block");
}];
}];
RACDisposable *disposable = [signal subscribeNext:^(id x) {
NSLog(@"next: %@", x);
} completed:^{
NSLog(@"completed");
}];
[RACScheduler.mainThreadScheduler afterDelay:1 schedule:^{
// async block 1
[disposable dispose];
}];
}

这段代码中有两个异步block,安排async block 1在1s时执行,async block 2在2s时执行。程序执行结果会怎样呢?

next: 1
dispose in didSubscribe block

结果有些令人诧异,为什么不是这样:

next: 1
next: 2
dispose in didSubscribe block
completed

把这个原因分析清楚并不是特别简单,因为要涉及一些其他角色。

先从RACDisposable讲起吧。顾名思义,它与「销毁对象」有关,站在程序的角度,它有一个block类型变量_disposeBlock,当向它第一次发送dispose消息时,该block会被调用。

除了RACDisposable之外,这里还要涉及它的子类RACCompoundDisposable,后者除了拥有RACDisposable特性外,它还可以管理多个子disposable,当向它发送dispose消息时,除了调用自身_disposeBlock,它也会向其子disposable发送该消息。

OK!开始从代码层面分析RACDynamicSignal的订阅过程了。

订阅信号(signal)时,首先会创建一个RACSubscriber对象(subscriber),如上文所述,subscriber和signal没有引用关系。

其次,内部还会创建一个RACCompoundDisposable实例(compound disposable),该实例会作为subscribeNext:error:completed:的返回值返回。

最后,内部还会创建一个RACPassthroughSubscriber实例(passthrough subscriber),passthrough subscriber是subscriber的透明代理,它有3个属性,分别指向subscriber、compound disposable以及signal。如下图所示:

complex@2x.png

如图,RACPassthroughSubscriber#disposableRACPassthroughSubscriber#innerSubscriber都是强引用,RACPassthroughSubscriber#signal是弱引用。

P.S: passthrough subscriber引用signal的目的似乎只是为了调试。

还需要说明的是,signal的didSubscribe block返回的disposable(可能为nil)会被添加到compound disposable中,作为其子dispose进行管理。

再次说明,compound disposable作为subscribeNext:error:completed:返回值返回,用户获取后,可以向其发送dispose消息。

RAC的设定是:当compound disposable被disposed时,passthrough subscriber就无法将sendNext:/sendError:/sendCompleted消息透传给subscriber。详见RACPassthroughSubscriber的实现代码:

// RACPassthroughSubscriber.m
- (void)sendNext:(id)value {
if (self.disposable.disposed) return; // <--这是关键
if (RACSIGNAL_NEXT_ENABLED()) { // 暂时不用理会这个`if`语句
RACSIGNAL_NEXT(cleanedSignalDescription(self.signal), cleanedDTraceString(self.innerSubscriber.description), cleanedDTraceString([value description]));
}
[self.innerSubscriber sendNext:value];
}

搞清楚了这个,就不难理解上面代码的执行结果了。如下这段代码的分析也类似:

{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
RACDisposable *disposable1 = [RACScheduler.mainThreadScheduler afterDelay:2 schedule:^{
[subscriber sendNext:@2];
}];
RACDisposable *disposable2 = [RACScheduler.mainThreadScheduler afterDelay:2 schedule:^{
[subscriber sendCompleted];
}];
return [RACDisposable disposableWithBlock:^{
[disposable1 dispose];
[disposable2 dispose];
NSLog(@"dispose!!!");
}];
RACDisposable *disposable = [signal subscribeNext:^(id x) {
NSLog(@"next: %@", x);
} completed:^{
NSLog(@"complete");
}];
[RACScheduler.mainThreadScheduler afterDelay:1 schedule:^{
[disposable dispose];
}];
}

P.P.S: didSubscribe的返回值disposable还是非常有用,举个例子,若didSubscribe的副作用是发起一个网络任务(譬如AFHTTPRequestOperation),则可以在该disposable的dispose block中执行[operation cancel]操作,当取消订阅时,就取消网络任务,因为它没有执行的必要了。

貌似看到过这样的说法:若向didSubscribe返回的disposable发送dispose消息,则subscriber的3个block都会被置为nil。我验证并查看了代码,这种说法是错误的。

来看段代码:

{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
return [RACDisposable disposableWithBlock:^{ // mark 1
NSLog(@"disposed");
}];
}];
[signal subscribeNext:^(id x) {
// ...
}]; // mark 3
NSLog(@"end");
} // mark 2

这段代码的执行结果是什么?是这样?

end
disposed

还是这样?

disposed
end

正确答案是后者。这个问题的本质是回答「mark 1处disposable的生命周期是怎么样的」,据上文的分析,它间接由passthrough subscriber持有,因此问题是:passthrough subscriber什么时候被释放?

我刚开始以为passthrough subscriber在mark 2处释放。事实不是这样的,passthrough subscriber在RACDynamic#subscribe:内部被创建,被传递给didSubscribe block,在mark 3处时,RACDynamic#subscribe:调用完毕,didSubscribe block也被同步调用了,不再有任何人持有它,故而被释放掉,连带着mark 1处的disposable被disposed掉…

如果代码改成这样:

{
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[RACScheduler mainThreadScheduler] afterDelay:1.0 schedule:^{
subscriber; // passthrough subscriber被GCD持有,1s后才被释放
}];
return [RACDisposable disposableWithBlock:^{ // mark 1
NSLog(@"disposed");
}];
}];
[signal subscribeNext:^(id x) {
// ...
}]; // mark 3
NSLog(@"end");
} // mark 2

留个问题:这段代码执行结果又会如何?

补充

现在(2017年末)再看这篇一年前的博客,有些汗颜,虽然没有明显的逻辑错误,但是结构太乱;当前对ReactiveCocoa的理解比当时要清楚得多,却没有当时的激情来对它进行重新整理了。今年早些时候在公司内部对为新人准备了一场ReactiveCocoa主题分享,此处贴上Keynote: ReactiveCocoa.key.zip,或许对看客有一点点用…