I've just read a tutorial about ReactiveCocoa.
In the "Avoiding Retain Cycles" chapter, the writer says, in order to avoid the retain cycle, we should replace self
with bself
in the subscribeNext
block. However he keeps self
in the map
block.
__weak RWSearchFormViewController *bself = self; // Capture the weak reference
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
bself.searchText.backgroundColor = color;
}];
Is this practice right? Why?
We can answer with science!
Although I laud that tutorial for mentioning retain cycles, I don't think it gives it a very good treatment (not to mention that it's buggy). It's actually a pretty easy thing to calculate when we need to @weakify
(though in the course of writing this I realized it's a difficult thing to explain clearly).
Let's look at the code again:
__weak typeof(self) weakSelf = self;
[[self.searchText.rac_textSignal map:^id(NSString *text) {
return [weakSelf isValidSearchText:text] ? [UIColor whiteColor] : [UIColor yellowColor];
}] subscribeNext:^(UIColor *color) {
weakSelf.searchText.backgroundColor = color;
}];
To answer "is there a retain cycle here", all we need to do is figure out when this whole thing will be deallocated. If the answer is "never", we know we have a problem.
So let's start at the very top level: the root of the signal is that rac_textSignal
. This signal will complete (and, unless we're explicitly retaining it elsewhere, deallocate) when its backing textView
or textField
is deallocated (I don't know which searchText
is). We can look in the implementation and see this:
return [[[[[RACSignal defer:...
] concat:...
] map:...
] takeUntil:self.rac_willDeallocSignal // <-- bingo (self is the text field here)
] setNameWithFormat:...];
So the "bottom level" signal will complete as soon as the UITextField
is no longer retained. And unless we add a take
or a takeUntil
or something else to change this fact, the entire signal chain will complete (and, fuzzily, be deallocated) as soon as that root signal does.
So now we have to ask when the UITextField
is deallocated.
If self
has a strong reference to it, the UITextField
won't deallocate until self
releases it (probably in [self dealloc]
). (In reality, the text field is probably in the view hierarchy as well, so it will need to come out before it can actually disappear, but let's assume this happens before self
is deallocated (it should, if our view controller code is good) and just pretend like self
is the only thing retaining it).
If self
has a weak reference to the text field, then we don't know and will need to gather more information to figure out what's keeping it alive. In that case, strongly referencing self in the block might cause a retain cycle, indirectly, but it might not.
Let's assume it's strong. Now the analysis is pretty straightforward: the text signal's not going away until the text field goes away. The text field's not going away until self
goes away. self
's not going away until...
Never. Because the blocks that the signal is retaining are retaining self
.
textField -> textSignal -> mapped signal
^ |
| |
+-- self <--- block <--+
Cycle town. The arrows here fuzzily mean "is keeping alive", not "is retaining", because the root text signal is not directly retaining the mapped signal, but the mapped signal's lifetime is nonetheless bound to the lifetime of the text signal.
So, using this analysis, we can conclude that yes, we want to weakly reference self
in both of those blocks.
But thinking about it in these terms helps us figure out in a more generic way when these weak references are necessary -- if you're just saying "weakly reference self when you're subscribing" as a rule of thumb (like the author of the tutorial), you'll make mistakes like this.
Now let's try hard mode:
UITextField *textField = self.searchText;
__weak typeof(self) weakSelf = self;
[[self.searchText.rac_textSignal map:^id(NSString *text) {
return [weakSelf isValidSearchText:text] ? [UIColor whiteColor] : [UIColor yellowColor];
}] subscribeNext:^(UIColor *color) {
textField.backgroundColor = color;
}];
Are we good? Of course not! Strongly referencing the text field in the block is just as bad, and this is every bit as much of a retain cycle as before. Here's what we have now:
textField -> textSignal -> mapped signal
^ ^ |
| | |
| +------------- block <---+
|
self
If we're in the mindset that "only self
can cause retain cycles" (a point of view common to block retain cycle blog tutorial-writers in general, not just in the ReactiveCocoa world) we'd never think to check for something like this. But if we spend a minute thinking about the lifetime of our signals, it's not too tricky to understand why self
isn't really special.
libextobjc
's EXTScope
really shines in situations like this. Rather than having to declare another "weak text field" variable, it allows us to add it to our @weakify
list and @strongify
it when we need to:
UITextField *textField = self.searchText;
@weakify(self, textField);
[[self.searchText.rac_textSignal map:^id(NSString *text) {
@strongify(self);
return [self isValidSearchText:text] ? [UIColor whiteColor] : [UIColor yellowColor];
}] subscribeNext:^(UIColor *color) {
@strongify(textField);
textField.backgroundColor = color;
}];
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