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.
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.)
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 (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.
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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With