Marcin Kapusta
Marcin Kapusta

Reputation: 5366

Not expected strange behaviour of ARC during deallocating instances

I'm refreshing my knowledge in Objective-C world and now I'm testing some ARC with __weak local variables.

I have very simple code with such files GAObject.h

#import <Foundation/Foundation.h>
@interface GAObject : NSObject
+ (instancetype)create;
@end

Implementation of this interface GAObject.h

#import "GAObject.h"
@implementation GAObject
+ (instancetype)create {
    return [[GAObject alloc] init];
}
- (void)dealloc {
    NSLog(@"GAObject is being deallocated");
}
@end

So there is simple factory method create and I override dealloc method to watch if objects was deallocated when I expecting this. Now the funny part main.m:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "Learning/GAObject.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"1");
        NSObject *o1 = [[GAObject alloc] init];
        NSObject * __weak weakObject = o1; // Line 1
        o1 = nil; // o1 should be deallocated because there is no strong references pointing to o1.
        NSLog(@"2");
        NSObject *o2 = [GAObject create]; // Line 2
        o2 = nil; // o2 should be deallocated here too but it is not deallocated. Why?
        NSLog(@"3");

        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

In the output I see this:

1
GAObject is being deallocated
2
3

But My expecting results should be:

1
GAObject is being deallocated
2
GAObject is being deallocated
3

If I create o2 using factory method then I have this behaviour. If I create o2 like this: [[GAObject alloc] init] then I get expected output. Also I noticed that when I remove line with weakObject I also get expected results. Can somebody explain it?

Upvotes: 0

Views: 56

Answers (1)

Ken Thomases
Ken Thomases

Reputation: 90521

It's because ARC still respects the Cocoa memory-management naming conventions.

Under those conventions, a method named +create returns a +0 reference. So, in the implementation of the method, ARC has to balance the +1 reference of the alloc/init pair by autoreleasing the reference.

Then, in main(), ARC has to assume it has received a +0 reference from the call to +create. If it needed the reference to survive the current scope, it would retain it, but it doesn't so it doesn't. The second GAObject instance would be deallocated when the autorelease pool is drained, but that will never happen because UIApplicationMain() never returns. If you use two separate autorelease pools, one for the code dealing with GAObjects and another for the call to UIApplicationMain(), I expect you'll get the result you expect.

If ARC did need the reference to survive, it would retain at the assignment to the strong variable and release when that variable is assigned a new value (including nil) or goes out of scope. ARC has a run-time optimization that an autorelease-return in a callee and a retain of the returned value in the caller cancel each other out, such that the object is never put in the autorelease pool. If this were happening, you would get your expected results.

In fact, my expectation is that the compiler initially would emit that retain and release even in your case, but a subsequent pass removes redundant retains and releases. Your example has the release immediately follow the retain, which makes it even more obvious to the compiler that the pair are redundant. Because the retain gets removed, that autorelease optimization doesn't kick in, and a reference to your object really does get put into the autorelease pool.

If your method were named +newGAObject, then the naming conventions would mean it returns a +1 reference and this all changes. (Of course, as it stands, your +create method is just doing the same thing as the built-in +new method, except for the autorelease that ARC has to add. So, you could just change the calling code to use +new and that would also sidestep this issue.)

I don't know why the line with weakObject matters. But, since the behavior you're seeing depends on certain optimizations, anything that could tweak the optimizations can change the outcome.

Upvotes: 2

Related Questions