Tony Friz
Tony Friz

Reputation: 893

Is it possible for dealloc to be called on an object whose retain count is NOT zero?

I'll keep this short and obvious: I have an object whose dealloc method is being called. I also have an NSTimer being called every 3 seconds to log to console the current retain count of said object.

Just to be clear: I know that NSTimer will retain the object. The situation still does not add up even when considering this.

Anyhow - at the time this timer fires, the retain count of the object is being logged as 3. This confuses me for 2 reasons:

  1. Why is dealloc being called if the object's retain count is not ever reaching 0?
  2. Since dealloc is getting called, shouldn't, at the very least, the retain count be 1 since the NSTimer instance is holding onto it?

Any help is greatly appreciated. Thank you.

Edit: code:

[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(logRetainCount) userInfo:nil repeats:YES];

^ set in viewDidLoad

- (void)logRetainCount
{
    NSLog(@"own retain count: %ld", CFGetRetainCount((__bridge CFTypeRef)self));
}

^ method that logs retain count

- (void)dealloc {
    NSLog(@"view controller deallocated");
}

^ dealloc method implemented in the VC that is supposed to be deallocated

And the console output:

own retain count: 5

view controller deallocated

own retain count: 3

Upvotes: 2

Views: 944

Answers (1)

Rob
Rob

Reputation: 438232

You ask:

Is it possible for dealloc to be called on an object whose retain count is NOT zero?

Since you're using ARC, we don't use "retain count" in that context anymore. But in answer to your question, no, an object cannot be deallocated while there are strong references. And when you call scheduledTimerWithTimeInterval, if that's a repeating timer, it will maintain a strong reference to the target, preventing the target from getting deallocated (at least until the timer's invalidate is called).

Consider:

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(logRetainCount) userInfo:nil repeats:YES];
}

- (void)logRetainCount {
    NSLog(@"own retain count: %ld", CFGetRetainCount((__bridge CFTypeRef)self));
}

- (void)dealloc {
    NSLog(@"view controller deallocated");
}

@end

When I push to this view controller, I see the following on the console:

2016-09-15 15:50:10.279 MyApp[7777:159811] own retain count: 3
2016-09-15 15:50:13.340 MyApp[7777:159811] own retain count: 3

And when I pop that view controller off, I see:

2016-09-15 15:50:16.338 MyApp[7777:159811] own retain count: 2
2016-09-15 15:50:19.270 MyApp[7777:159811] own retain count: 2

Note, we do not see "view controller deallocated" appear in the console.

When I click on Xcode 8's "Debug Memory Graph" button, we can see that the timer is still keeping a strong reference to it:

timer

You ask:

  1. Why is dealloc being called if the object's retain count is not ever reaching 0?

It can't be. So, we must have multiple instances of the view controller involved here, one that has the repeating timer that isn't deallocated, and one without the timer, that is deallocated when its last strong reference is resolved. But whatever object is the target of the timer will still have a strong reference to it until the timer is invalidated, and it will not be deallocated until the timer has its invalidate called.

  1. Since dealloc is getting called, shouldn't, at the very least, the retain count be 1 since the NSTimer instance is holding onto it?

No, while the repeating timer is firing, its target cannot be deallocated. We must be talking about multiple instances of the view controller (or, unlike the example above, the target of the timer wasn't the view controller).

There are lots of ways to accidentally introduce additional instances of a view controller. For a random example (one that I've seen here on Stack Overflow more than once), consider that you did a segue between two view controllers, and had a prepareForSegue that did something like:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    SecondViewController *secondViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    secondViewController.property = @"foo";
}

This is incorrect, because in addition to the view controller that the segue instantiated (the segue.destinationViewController), the above prepareForSegue will create another instance. The one created in prepareForSegue will be deallocated as soon as it falls out of scope, but the one created by the segue will not be deallocated because of the repeating timer that is created in viewDidLoad.

I'm not suggesting that this is what you did, but it illustrates one possible way to get the behavior you describe.

But, in short, no, in ARC, an object that still has any strong references will not be deallocated. Only when the last remaining strong reference is removed will it be deallocated. The code in your question cannot, alone, produce the behavior you describe. You must be dealing with some additional instance of the view controller or something curious like that. I might suggest you create a simple example that reproduces the problem you describe because the code in your question does not. There's something else going on.

Upvotes: 5

Related Questions