ReactiveCoCoa 内存管理

概述

ReactiveCoCoa的内存管理相当的复杂,不过这导致的最终结果是:你可以不通过持有信号来处理他们

如果ReactiveCoCoa框架要求你持有每一个信号,它使用起来将会很笨重,对于像网络请求等这些在未来使用的一次性信号来说,更是如此。你必须将长期存在的信号保存在属性中,当你完成相关操作后,还要确保将其清除,这很没意思。

订阅者(Subscribers)

在进一步讨论之前,需要注意的是:subscribeNext:error:completed:(及其变体)会在block内隐式创建订阅者。因此在block中持有的任何对象,都会被订阅者持有。就像其他任何对象一样,self如果没有直接或间接引用它,它们 将不会被保留。

有限或者短期的信号(Finite Or Short-Lived Signals)

RAC内存管理最重要的指导原则是,订阅关系会在完成或者发生错误后终结,并且订阅者会被移除。

举个栗子,假如你有如下代码在view controller里

1
2
3
self.disposable = [signal subscribeCompleted:^{
doSomethingPossiblyInvolving(self);
}];

内存管理就像这样:

1
view controller -> RACDisposable -> RACSignal -> RACSubscriber -> view controller

然而,一旦signal结束这种RACSignal -> RACSubscriber关系就会被拆除 ,从而打破了引用环。
通常,这正是你所需要的,因为RACSignal的生命周期自然会匹配事件流的逻辑生命周期。

无限信号(Infinite Signals)

无线信号(或者存活时间太久以致它们可能是无限的信号),并不会自动释放。这时候disposables就派上用场了
处理(也就是调用disposabledispose方法)订阅关系将会移除关联的订阅者,,并将优雅地清理相关的资源。对于订阅者而言,就好像信号已经完成或者发生了错误。其他的订阅者并不受影响。
如果您必须手动管理订阅的生命周期,那么可能有更好的方法来执行您想要的操作

来自于self的信号(Signals Derived from self)

虽然,这里仍然存在一些棘手的中间情况。任何时候信号的生命周期都与调用范围联系在一起,你将有一个更难打破的引用环。
这通常发生在使用RACObserve()关联self的key path,并需要使用block捕获self的时候。
最简单的办法是使用__weak

1
2
3
4
5
__weak id weakSelf = self;
[RACObserve(self, username) subscribeNext:^(NSString *username) {
id strongSelf = weakSelf;
[strongSelf validateUsername];
}];

或者,在引入 EXTScope.h 头文件后

1
2
3
4
5
@weakify(self);
[RACObserve(self, username) subscribeNext:^(NSString *username) {
@strongify(self);
[self validateUsername];
}];

(如果对象不支持弱引用,分别使用,unsafe_unretained或者@unsafeify替换weak或@weakify)

不过,你可以使用更好的模式。例如,上面的例子可以写成

1
[ self  rac_liftSelector:@selector(validateUsername:)withSignals:RACObserve(self,username),nil ];

或者

1
2
3
4
RACSignal * validated = [ RACObserve(self,username)map: ^(NSString * username){
//将验证逻辑放在这里。
return @ YES ;
}];

跟无限信号一样,这可以避免在信号流的block中持有self


为了有效使用ReactiveCocoa,以上就是你所需要的全部信息。然而,还有一点需要解决,只是出于技术上的好奇,或者任何对RAC有贡献的人。

“不需要持有”的设计目标引出一个问题:我们怎么知道一个信号何时被释放?如果只是创建而没有被持有呢?
答案是我们无法知道,但是我们通常可以假设调用者会在当前运行循环迭代中保留信号,如果他们想保留它的话。
所以:

  1. 创建的信号会自动添加到全局活动信号集。
  2. 该信号将等待主线程RunLoop的一次循环,然后如果它没有订户,则从活动集中移出。除非信号以某种方式保留,否则它将在此处释放
  3. 如果信号在RunLoop迭代中被订阅了,它将会被保留在信号集里
  4. 当所有订阅者都被移除了,步骤2就会被触发

如果RunLoop以递归的方式运转,这将适得其反。不过大多数或全部情况下ReactiveCocoa框架会简化使用者的代码。