28

当我创建一个信号并将其带入函数范围时,根据 Cocoa 约定,它的有效保留计数为 0:

RACSignal *signal = [self createSignal];

当我订阅信号时,它会保留订阅者并返回一个一次性的,根据 Cocoa 约定,它的保留计数也为零。

RACDisposable *disposable = [signal subscribeCompleted:^ {
    doSomethingPossiblyInvolving(self);
}];

大多数情况下,订阅者将关闭并引用self其 ivars 或封闭范围的其他部分。因此,当您订阅信号时,该信号具有对订阅者的拥有引用,而订阅者具有对您的拥有引用。并且您获得的一次性产品具有对信号的拥有引用。

disposable -> signal -> subscriber -> calling scope

假设您持有该一次性用品,以便您可以在某个时候取消订阅(例如,如果信号正在从 Web 服务检索数据并且用户导航离开屏幕,取消她查看正在检索的数据的意图)。

self.disposeToCancelWebRequest = disposable;

此时我们有一个循环引用:

calling scope -> disposable -> signal -> subscriber -> calling scope

负责任的事情是确保在取消请求时或在请求完成后中断循环。

 [self.disposeToCancelWebRequest dispose]
 self.disposeToCancelWebRequest = nil;

self请注意,在被释放时您不能这样做,因为由于保留周期,这永远不会发生!在回调订阅者期间打破保留周期似乎也有些可疑,因为信号可能在其实现仍在调用堆栈上时被释放。

我还注意到该实现保留了一个活动信号的全局进程列表(截至我最初提出这个问题时)。

使用 RAC 时我应该如何考虑所有权?

4

2 回答 2

55

ReactiveCocoa 的内存管理是相当复杂的,老实说,但有价值的最终结果是您不需要保留信号来处理它们

如果框架要求您保留每个信号,那么使用起来会更加笨拙,尤其是对于像期货一样使用的一次性信号(例如,网络请求)。您必须将任何长期存在的信号保存到属性中,然后确保在完成后将其清除。不好玩。

订户

在继续之前,我应该指出subscribeNext:error:completed:(及其所有变体)使用给定的块创建一个隐式订阅者。因此,从这些块引用的任何对象都将保留为订阅的一部分。就像任何其他对象一样,self如果没有直接或间接引用它,就不会被保留。

(根据您问题的措辞,我认为您已经知道这一点,但它可能对其他人有帮助。)

有限或短期信号

RAC 内存管理最重要的准则是订阅在完成或错误时自动终止,并且订阅者被删除。要使用您的循环引用示例:

calling scope -> disposable -> signal -> subscriber -> calling scope

......这意味着signal -> subscriber关系一旦signal完成就会被拆除,从而打破保留周期。

这通常是您所需要的,因为RACSignal内存中的生命周期自然会匹配事件流的逻辑生命周期。

无限信号

然而,无限信号(或寿命如此之长以至于它们可能是无限的信号)永远不会自然而然地消失。这就是一次性用品大放异彩的地方。

处置订阅将删除关联的订阅者,并且通常只清理与该订阅关联的所有资源。对于那个订阅者来说,就好像信号已经完成或出错了,只是没有在信号上发送最终事件。所有其他订阅者将保持不变。

但是,作为一般经验法则,如果您必须手动管理订阅的生命周期,则可能有更好的方法来做您想做的事。-take:or之类的方法-takeUntil:将为您处理处置,您最终会得到更高级别的抽象。

来自的信号self

不过,这里仍然有一个棘手的中间情况。任何时候信号的生命周期都与调用范围相关联,您将有一个更难打破的循环。

这通常发生在使用RACAble()RACAbleWithStart()在相对于 的关键路径上self,然后应用需要捕获的块时self

这里最简单的答案就是捕获self

__weak id weakSelf = self;
[RACAble(self.username) subscribeNext:^(NSString *username) {
    id strongSelf = weakSelf;
    [strongSelf validateUsername];
}];

或者,在导入包含的EXTScope.h标头后:

@weakify(self);
[RACAble(self.username) subscribeNext:^(NSString *username) {
    @strongify(self);
    [self validateUsername];
}];

(如果对象不支持弱引用,请分别用or替换__weakor 。)@weakify__unsafe_unretained@unsafeify

但是,您可能可以使用更好的模式。例如,上面的示例也许可以写成:

[self rac_liftSelector:@selector(validateUsername:)
           withObjects:RACAble(self.username)];

或者:

RACSignal *validated = [RACAble(self.username) map:^(NSString *username) {
    // Put validation logic here.
    return @YES;
}];

与无限信号一样,通常有一些方法可以避免self从信号链中的块引用(或任何对象)。


为了有效地使用 ReactiveCocoa,以上信息确实是您所需要的。然而,我想再谈一点,只是为了技术上的好奇或任何有兴趣为 RAC 做贡献的人:

我还注意到该实现保留了一个进程全局活动信号列表。

这是绝对正确的。

“无需保留”的设计目标引出了一个问题:我们如何知道何时应该释放信号?如果它刚刚创建,从自动释放池中逃脱,并且还没有被保留怎么办?

真正的答案是我们不这样做,但我们通常可以假设调用者将在当前运行循环迭代中保留信号,如果他们想保留它。

最后:

  1. 创建的信号会自动添加到一组全局活动信号中。
  2. 该信号将等待主运行循环的单次通过,然后如果它没有订阅者,则将其从活动集中移除。除非信号以某种方式被保留,否则它将在此时解除分配。
  3. 如果在该运行循环迭代中确实订阅了某些东西,则信号将保留在集合中。
  4. 稍后,当所有订阅者都消失时,再次触发#2。

如果运行循环是递归旋转的(就像在 OS X 上的模态事件循环中),这可能会适得其反,但对于大多数或所有其他情况,它会使框架使用者的生活变得更加容易。

于 2012-12-31T12:33:21.423 回答
0

我正在尝试解决 ReactiveCocoa 2.5 的内存管理之谜

RACSubject* subject = [RACSubject subject];
RACSignal* signal = [RACSignal return:@(1)];
NSLog(@"Retain count of RACSubject %ld", CFGetRetainCount((__bridge CFTypeRef)subject));
NSLog(@"Retain count of RACSignal %ld", CFGetRetainCount((__bridge CFTypeRef)signal));

第一行输出1,第二行输出2。似乎RACSignal会保留在某个地方,而RACSubject不是。如果您没有明确地保留RACSubject,它将在程序退出当前范围时被释放。

于 2016-05-05T16:33:12.940 回答