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

bva*_*een 28 cocoa reactive-programming reactive-cocoa

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

RACSignal *signal = [self createSignal];
Run Code Online (Sandbox Code Playgroud)

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

RACDisposable *disposable = [signal subscribeCompleted:^ {
    doSomethingPossiblyInvolving(self);
}];
Run Code Online (Sandbox Code Playgroud)

大多数情况下,订户将关闭并引用self其ivars或封闭范围的其他部分.因此,当您订阅信号时,信号具有对订户的拥有参考,并且订户具有您自己的参考.你得到的一次性用品有一个信号的拥有参考.

disposable -> signal -> subscriber -> calling scope
Run Code Online (Sandbox Code Playgroud)

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

self.disposeToCancelWebRequest = disposable;
Run Code Online (Sandbox Code Playgroud)

此时我们有一个循环参考:

calling scope -> disposable -> signal -> subscriber -> calling scope
Run Code Online (Sandbox Code Playgroud)

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

 [self.disposeToCancelWebRequest dispose]
 self.disposeToCancelWebRequest = nil;
Run Code Online (Sandbox Code Playgroud)

请注意,self在释放时不能执行此操作,因为保留周期永远不会发生这种情况!在回调用户期间,在打破保留周期方面看起来似乎有些可疑,因为信号可能在其实现仍然在调用堆栈上时被释放.

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

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

Jus*_*ers 55

说实话,ReactiveCocoa的内存管理非常复杂,但最终的结果是你不需要保留信号来处理它们.

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

认购

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

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

有限或短期信号

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

calling scope -> disposable -> signal -> subscriber -> calling scope
Run Code Online (Sandbox Code Playgroud)

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

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

无限信号

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

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

但是,作为一般经验法则,如果您必须手动管理订阅的生命周期,则可能有更好的方法来执行您想要的操作.喜欢-take:-takeUntil:将为您处理处理的方法,最终得到更高级别的抽象.

来自的信号 self

不过,这里仍有一些棘手的中间案例.每当信号的生命周期与调用范围相关联时,您将有一个更难以打破的周期.

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

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

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

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

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

(如果对象不支持弱引用,则分别替换__weak@weakifywith __unsafe_unretained或者@unsafeify.)

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

[self rac_liftSelector:@selector(validateUsername:)
           withObjects:RACAble(self.username)];
Run Code Online (Sandbox Code Playgroud)

要么:

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

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


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

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

这绝对是真的.

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

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

所以:

  1. 创建的信号自动添加到一组全局活动信号中.
  2. 信号将等待主运行循环的单次传递,然后如果没有订户则从活动集中移除自身.除非以某种方式保留信号,否则它将在此时解除分配.
  3. 如果在该运行循环迭代中订阅了某些内容,则该信号将保留在该集合中.
  4. 之后,当所有订阅者都离开时,#2再次被触发.

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

  • 此信息已添加到RAC文档中:https://github.com/github/ReactiveCocoa/blob/master/Documentation/MemoryManagement.md (2认同)