Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory management in ReactiveCocoa

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?

like image 420
Henry H Miao Avatar asked Jul 13 '14 07:07

Henry H Miao


1 Answers

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;
}];
like image 121
Ian Henry Avatar answered Nov 04 '22 14:11

Ian Henry