ReactiveCocoa的引用所有权语义是什么? [英] What are the reference ownership semantics of ReactiveCocoa?

查看:94
本文介绍了ReactiveCocoa的引用所有权语义是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  RACSignal * signal = [self createSignal]; 

当我订阅信号时,它保留订阅者并返回一个可执行文件,也有一个保留计数为零。

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

大多数时候,订阅者将关闭并引用 self 或其ivars或封装范围的某些其他部分。因此,当您订阅信号时,信号具有对订户的拥有引用,并且订户具有对您的拥有引用。

  disposable  - >信号→>订户 - >调用范围

假设你持有一次性,所以你可以在某个时候取消你的订阅,如果信号正在从web服务检索数据并且用户导航离开屏幕,取消她想要查看正在检索的数据)。

  self.disposeToCancelWebRequest = 

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

 调用范围 - >一次性 - 信号→>订户 - >调用范围

负责的事情是确保在取消请求时请求已完成。

  [self.disposeToCancelWebRequest dispose] 
self.disposeToCancelWebRequest = nil;请注意,当 self 时,您不能这样做。


$ b <正在被解除分配,因为这将永远不会发生,由于保留周期!在回调给订阅者的过程中,由于信号可能被释放而其实现仍然在调用栈上,所以在打破保持周期方面似乎很讨厌。



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



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

解决方案

ReactiveCocoa的内存管理是相当复杂的,老实说,但最值得的结果是你不需要保留信号以处理它们



如果框架要求你保留每个信号,那么使用起来会更加困难,特别是对于像期货(例如网络请求)的单次信号。你必须将任何长寿命的信号保存到一个属性,然后还要确保在你完成它后清除它。



订阅者



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





有限或短期内,您的问题可能会有所不同,生存信号



RAC内存管理的最重要指南是:订阅在完成或错误后自动终止,订阅者已删除。要使用循环引用示例:

 调用范围 - >一次性 - 信号→>订户 - >调用范围

...这意味着信号 - 一旦信号完成,用户关系就会消失。打破保留周期。



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



无限信号



无限信号(或生命时间长到可以无限的信号)永远不会自然地撕裂。



处理订阅会移除相关联的订阅者,并且通常会清除与该订阅相关联的任何资源。对于那个用户,它就像信号已经完成或错误,除了没有最终事件发送信号。



但是,作为一般的经验法则,如果您必须手动管理订阅的生命周期,可能有更好的方法 -take: -takeUntil:的方法将处理您的处置,



信号来自 self



这里还有一个棘手的中间情况。



当使用 RACAble时,通常会出现这种情况在与 self 相对的键路径上的 RACAbleWithStart()应用需要捕获 self 的块。



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

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

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

  @weakify ); 
[RACAble(self.username)subscribeNext:^(NSString * username){
@strongify(self);
[self validateUsername];
}];
__ weak @weakify 分别使用 __ unsafe_unretained @unsafeify 对象不支持弱引用。)



但是,可能有更好的模式。上面的例子可能写成:

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

或:

 code> RACSignal * validated = [RACAble(self.username)map:^(NSString * username){
//在这里放入验证逻辑。
return @YES;
}];

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






你应该需要为了有效地使用ReactiveCocoa。但是,我想再谈一点,只是为了技术上的好奇或任何有兴趣参与RAC的人:



这是绝对真实的。



不需要保留的设计目标提出了一个问题:我们如何知道何时应该释放信号?如果它刚刚创建,转出自动释放池,并且尚未保留,该怎么办?



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



因此:


  1. 创建的信号会自动添加到全局活动信号集。

  2. 信号将等待单次通过主运行循环,然后如果没有订阅者,则将其从活动集中删除

  3. 如果在该运行循环迭代中有订阅,则该信号会保留在该集合中。

  4. 后来,当所有订阅者都消失后,#2会再次触发。

递归地(像在OS X上的模态事件循环),但它使得框架消费者的生活对于大多数或所有其他情况更容易。


When I create a signal and bring it into the scope of a function, its effective retain count is 0 per Cocoa conventions:

RACSignal *signal = [self createSignal];

When I subscribe to the signal, it retains the subscriber and returns a disposable which, per Cocoa conventions, also has a retain count of zero.

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

Most of the time, the subscriber will close over and reference self or its ivars or some other part of the enclosing scope. So when you subscribe to a signal, the signal has an owning reference to the subscriber and the subscriber has an owning reference to you. And the disposable you get in return has an owning reference to the signal.

disposable -> signal -> subscriber -> calling scope

Suppose you hold on to that disposable so you can cancel your subscription at some point (for instance, if the signal is retrieving data from a web service and the user navigates away from the screen, canceling her intent to view the data being retrieved).

self.disposeToCancelWebRequest = disposable;

At this point we have a circular reference:

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

The responsible thing to do is to ensure that the cycle is broken when canceling a request or after a request has completed.

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

Note that you cannot do this when self is being deallocated, because that will never happen due to the retain cycle! Something also seems fishy about breaking the retain cycle during a callback to the subscriber, since the signal could potentially be deallocated whilst its implementation is still on the call stack.

I also notice that the implementation retains a process-global list of active signals (as of the time I am originally asking this question).

How should I think about ownership when using RAC?

解决方案

ReactiveCocoa's memory management is quite complex, to be honest, but the worthy end result is that you don't need to retain signals in order to process them.

If the framework required you to retain every signal, it'd be much more unwieldy to use, especially for one-shot signals that are used like futures (e.g., network requests). You'd have to save any long-lived signal into a property, and then also make sure to clear it out when you're done with it. Not fun.

Subscribers

Before going any further, I should point out that subscribeNext:error:completed: (and all variants thereof) create an implicit subscriber using the given blocks. Any objects referenced from those blocks will therefore be retained as part of the subscription. Just like any other object, self won't be retained without a direct or indirect reference to it.

(Based on the phrasing of your question, I think you already knew this, but it might be helpful for others.)

Finite or Short-Lived Signals

The most important guideline to RAC memory management is that a subscription is automatically terminated upon completion or error, and the subscriber removed. To use your circular reference example:

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

… this means that the signal -> subscriber relationship is torn down as soon as signal finishes, breaking the retain cycle.

This is often all you need, because the lifetime of the RACSignal in memory will naturally match the logical lifetime of the event stream.

Infinite Signals

Infinite signals (or signals that live so long that they might as well be infinite), however, will never tear down naturally. This is where disposables shine.

Disposing of a subscription will remove the associated subscriber, and just generally clean up any resources associated with that subscription. To that one subscriber, it's just as if the signal had completed or errored, except no final event is sent on the signal. All other subscribers will remain intact.

However, as a general rule of thumb, if you have to manually manage a subscription's lifecycle, there's probably a better way to do what you want. Methods like -take: or -takeUntil: will handle disposal for you, and you end up with a higher-level abstraction.

Signals Derived from self

There's still a bit of a tricky middle case here, though. Any time a signal's lifetime is tied to the calling scope, you'll have a much harder cycle to break.

This commonly occurs when using RACAble() or RACAbleWithStart() on a key path that's relative to self, and then applying a block that needs to captures self.

The easiest answer here is just to capture self weakly:

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

Or, after importing the included EXTScope.h header:

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

(Replace __weak or @weakify with __unsafe_unretained or @unsafeify, respectively, if the object doesn't support weak references.)

However, there's probably a better pattern you could use instead. For example, the above sample could perhaps be written like:

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

or:

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

As with infinite signals, there are generally ways you can avoid referencing self (or any object) from blocks in a signal chain.


The above information is really all you should need in order to use ReactiveCocoa effectively. However, I want to address one more point, just for the technically curious or for anyone interested in contributing to RAC:

I also notice that the implementation retains a process-global list of active signals.

This is absolutely true.

The design goal of "no retaining necessary" begs the question: how do we know when a signal should be deallocated? What if it was just created, escaped an autorelease pool, and hasn't been retained yet?

The real answer is we don't, BUT we can usually assume that the caller will retain the signal within the current run loop iteration if they want to keep it.

Consequently:

  1. A created signal is automatically added to a global set of active signals.
  2. The signal will wait for a single pass of the main run loop, and then remove itself from the active set if it has no subscribers. Unless the signal was retained somehow, it would deallocate at this point.
  3. If something did subscribe in that run loop iteration, the signal stays in the set.
  4. Later, when all the subscribers are gone, #2 is triggered again.

This could backfire if the run loop is spun recursively (like in a modal event loop on OS X), but it makes the life of the framework consumer much easier for most or all other cases.

这篇关于ReactiveCocoa的引用所有权语义是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆