ReactiveCocoa的参考所有权语义是什么?

时间:2012-12-31 10:03:01

标签: cocoa reactive-programming reactive-cocoa

当我创建一个信号并将其带入函数范围时,每个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时如何考虑所有权?

2 个答案:

答案 0 :(得分:54)

ReactiveCocoa的内存管理非常复杂,说实话,但最有效的结果是您不需要保留信号以便处理它们

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

订户

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

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

有限或短暂信号

RAC内存管理最重要的指导原则是订阅会在完成或错误时自动终止,订阅者将被删除。要使用循环参考示例:

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

...这意味着signal -> subscriber关系会在signal完成后立即拆除,从而打破保留周期。

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

无限信号

然而,无限信号(或长寿以至于它们可能无限的信号)将永远不会自然消失。这是一次性用品闪耀的地方。

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

但是,作为一般经验法则,如果您必须手动管理订阅的生命周期,则可能有更好的方法来执行您想要的操作。类似-take:或{{的方法1}}会处理你的处理,你最终得到更高级别的抽象。

来自-takeUntil:

的信号

尽管如此,这里仍然有一些棘手的中间案例。每当信号的生命周期与调用范围相关联时,你就会有更难的周期。

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

这里最简单的答案就是弱地捕捉self

self

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

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

(如果对象不支持弱引用,则分别将@weakify(self); [RACAble(self.username) subscribeNext:^(NSString *username) { @strongify(self); [self validateUsername]; }]; __weak替换为@weakify__unsafe_unretained。) < / p>

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

@unsafeify

或:

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

与无限信号一样,通常有一些方法可以避免从信号链中的块引用RACSignal *validated = [RACAble(self.username) map:^(NSString *username) { // Put validation logic here. return @YES; }]; (或任何对象)。


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

  

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

这是绝对正确的。

“不保留必要”的设计目标引出了一个问题:我们如何知道何时应该取消分配信号?如果它刚刚创建,转出自动释放池并且还没有被保留呢?

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

因此:

  1. 创建的信号会自动添加到一组全局活动信号中。
  2. 信号将等待主运行循环的单次传递,然后如果没有订户,则将其自身从活动集中移除。除非信号以某种方式保留,否则它将在此时解除分配。
  3. 如果在该运行循环迭代中订阅了某些内容,则该信号将保留在该集合中。
  4. 稍后,当所有订阅者都离开时,#2再次被触发。
  5. 如果运行循环以递归方式旋转(如在OS X上的模态事件循环中),这可能会适得其反,但是对于大多数或所有其他情况,它会使框架使用者的生活变得更加容易。

答案 1 :(得分: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,那么当程序退出当前范围时,它将被取消分配。