Why is Objective-C ARC deallocation dependent on w

2019-07-18 19:41发布

问题:

I'm learning about ARC memory management and ran across something that doesn't make sense to me.

In the example code below, an object that is allocated locally in main() gets deallocated when its pointer is assigned to nil, as I would expect.

But if the same type of object is allocated in another function, and a pointer to it is defined in main(), and that pointer is set to nil, the object does not get deallocated until main function exits. That is mysterious to me.

In the code below, two instances of class GRMemoryChecker are created. In one case, it's allocated directly in main(), and in the other, main() calls itemMakerFunc() to do the allocation. When you run main(), the log output shows that the instance allocated in the function is not deallocated when its pointer is set to nil -- it's deallocated when the function exits.

I assume that means that the itemMakerFunc()-created instance has 2 owners before the pointer is set to nil, whereas the locally-created instance has only 1.

But why? What other owner is still in existence at the time the pointer is set to nil? Or if no other owner still exists, why wasn't the counter decremented when it went out of existence?

GRMemoryChecker.h:

#import <Foundation/Foundation.h>

@interface GRMemoryChecker : NSObject
{
    NSString *name;
}
- (id)initWithName:(NSString *)str;

- (void)setName:(NSString *)str;

- (void) dealloc;
@end

GRMemoryChecker.m:

#import "GRMemoryChecker.h"

@implementation GRMemoryChecker

- (id)initWithName:(NSString *)str
{
    self = [super init];
    if (self)
    {
        [self setName:str];
    }
    return self;
}

- (void)setName:(NSString *)str
{
    name = str;
}

- (NSString *)description
{
    return name;
}

- (void) dealloc;
{
    NSLog(@"Destroyed: %@", self);
}
@end

main.m:

#import <Foundation/Foundation.h>
#import "GRMemoryChecker.h"

GRMemoryChecker *itemMakerFunc()
{
    return [[GRMemoryChecker alloc] initWithName:@"func-based checker" ];
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        GRMemoryChecker *checkerLocallyCreated = [[GRMemoryChecker alloc] initWithName:@"locally-created checker"];
        GRMemoryChecker *checkerFuncBased = itemMakerFunc();

        NSLog(@"Before setting func-based checker pointer to nil");
        checkerFuncBased = nil;
        NSLog(@"After setting func-based checker pointer to nil");

        NSLog(@"Before setting locally-created checker pointer to nil");
        checkerLocallyCreated = nil;
        NSLog(@"After setting locally-created checker pointer to nil");
    }
    return 0;
}

Console Output:

Before setting func-based checker pointer to nil
After setting func-based checker pointer to nil
Before setting locally-created checker pointer to nil
Destroyed: locally-created checker
After setting locally-created checker pointer to nil
Destroyed: func-based checker

回答1:

ARC enforces the same memory management semantics you're supposed to follow when you manually retain and release your objects. So let's look at how we would write this under MRR and see if that tells us anything:

GRMemoryChecker *itemMakerFunc()
{
    return [[[GRMemoryChecker alloc] initWithName:@"func-based checker" ] autorelease];
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        GRMemoryChecker *checkerLocallyCreated = [[GRMemoryChecker alloc] initWithName:@"locally-created checker"];
        GRMemoryChecker *checkerFuncBased = itemMakerFunc();

        NSLog(@"Before setting func-based checker pointer to nil");
        checkerFuncBased = nil;
        NSLog(@"After setting func-based checker pointer to nil");

        NSLog(@"Before setting locally-created checker pointer to nil");
        [checkerLocallyCreated release];
        checkerLocallyCreated = nil;
        NSLog(@"After setting locally-created checker pointer to nil");
    }
    return 0;
}

So basically, the reference returned by alloc is an owning reference, which needs to be released. When an object only lives in one function, we can release it directly and it will be immediately deallocated. But when we're returning it from the function, we can't simply release it, because it needs to live past the return statement, so it gets autoreleased.



回答2:

It does not depend on whether the object is created in a function or not, but if the function has a "retained return value" or a "unretained return value" (see Objective-C Automatic Reference Counting).

By default, only methods in the alloc, copy, init, mutableCopy, and new families have retained return values, meaning that the function returns a (+1) retained object.

All other functions have an unretained return value. Simplifying things a bit, you can say that these functions return an autoreleased value (subject to optimizations made by the ARC compiler) to ensure that the returned value is valid in the calling function.

The autoreleased value is deallocated when the current autorelease pool is destroyed.

You can change the behavior of your function with the NS_RETURNS_RETAINED attribute:

GRMemoryChecker *itemMakerFunc() NS_RETURNS_RETAINED;
GRMemoryChecker *itemMakerFunc()
{
    return [[GRMemoryChecker alloc] initWithName:@"func-based checker" ];
}

Now the function returns a retained value, which is deallocated immediately when you remove the strong reference by setting checkerFuncBased = nil:

Before setting func-based checker pointer to nil
Destroyed: func-based checker
After setting func-based checker pointer to nil
Before setting locally-created checker pointer to nil
Destroyed: locally-created checker
After setting locally-created checker pointer to nil


回答3:

First of all, your dealloc method is leaking. You have to call [super dealloc] in it or the object is never deallocated.

About the different behavior of both instances, as you are returning an allocated object in itemMakerFunc, ARC will apply "autorelease" when returning. So, that instance is autoreleased but not released immediately when other retainers release it. On the other hand, the other instance is retained and then, when assigning nil to the variable, is released immediately. Think that autorelease is a postposed action. It will be released when the current loop ends but not immediately.