Maksim Kondratyuk
Maksim Kondratyuk

Reputation: 1399

Why is memory sometimes deallocated immediately and other times only when autorelease pool is drained?

I made simple experiment and found some strange behavior. Here some code - part of long method with ARC enabled:

MKObject *obj = [[MKObject alloc]init];
NSMutableArray *temp = [[NSMutableArray alloc]init];
[temp addObject:obj];
obj = nil;
temp = nil;
//here deallocating is called on obj (MKObject)
//other stuff

but if I change NSMutableArray to NSArray and literal initialisation

NSArray *temp = @[obj];

deallocating executed before autoreleasepool closed, not after setting nil to all references. Did I missed something ?

Upvotes: 1

Views: 248

Answers (3)

mc01
mc01

Reputation: 3780

A couple of things I see, though I'm not entirely clear on the question so I can't say which applies:

Adding an object to an NSArray or NSMutableArray increments the object's retain count. In the first instance, you manually instantiate obj, which gives it retain count 1. Adding it to the NSMutableArray makes its retain count 2. In this case, obj = nil decrements retain to 1; temp = nil tells the array to handle releasing its contents. Those w/retain count 0 get dealloc'd immediately.

In the 2nd instance with @[] literal creation, the literal syntax under the hood creates an autoreleased object using the method arrayWithObjects: count:. When this is no longer needed it goes to the autorelease pool for eventual deallocation.

It isn't an issue of the objects IN the array but the way the arrays themselves were created.

Edited my original response to address comments below - I was confusing the issue.

Upvotes: 1

Rob
Rob

Reputation: 437917

A few observations:

  1. In your first example, neither the MKObject nor the NSMutableArray is an autorelease object, so these objects will be deallocated immediately, not waiting for the autorelease pool to drain:

    MKObject *obj = [[MKObject alloc] init];
    NSMutableArray *temp = [[NSMutableArray alloc] init];
    [temp addObject:obj];
    obj = nil;
    temp = nil;
    
  2. In your second example, the NSArray is an autorelease object, so the NSArray (and therefore, the MKObject) will not be deallocated until the autorelease pool is drained.

    MKObject *obj = [[MKObject alloc] init];
    NSArray *temp = @[obj];
    obj = nil;
    temp = nil;
    

    To understand why the array literal, @[], creates an autorelease object, one should note that it expands to +[NSArray arrayWithObjects:count:]. Autorelease objects are created whenever you instantiate an object with any method other than using alloc followed by an init (whether init, or one of the permutations, such as initWithObjects:).

  3. As you observed, when an app creates autorelease object, the object will not be immediately be deallocated, but it will when the autorelease pool drains. Since we generally yield back to the runloop quickly (at which point the pool will be drained), the choice of autorelease objects or non-autorelease objects has little practical impact in simple cases. But if the app, for example, has a for loop in which it creates many autorelease objects without yielding back to the runloop, it could be problematic (especially if MKObject was large or you were doing this many times). For example:

    for (NSInteger i = 0; i < 100; i++) {
        MKObject *obj = [[MKObject alloc] init];
        NSArray *temp = @[obj];
    
        // Note, because they are local variables which are falling out of scope, I don't have to manually `nil` them.
    }
    

    Because we are instantiating autorelease NSArray objects in this example, the above would keep all 100 arrays and objects in memory until you yielded back to the runloop and the autorelease pool had a chance to drain. This means that the app's "high water mark" (the maximum amount memory it uses at any given time), would be higher than it might need to be. You could remedy this by either:

    • use a non-autorelease object (such as by using alloc/init) instead of using the array literal:

      for (NSInteger i = 0; i < 100; i++) {
          MKObject *obj = [[MKObject alloc] init];
          NSArray *temp = [[NSArray alloc] initWithObjects:obj, nil];
      }
      

    or

    • by introducing your own, explicitly declared @autoreleasepool:

      for (NSInteger i = 0; i < 100; i++) {
          @autoreleasepool {
              MKObject *obj = [[MKObject alloc] init];
              NSArray *temp = @[obj];
          }
      }
      

    In this final example, the autorelease pool will be drained for each iteration of the for loop, resolving any challenges with autorelease objects that would otherwise have their deallocation deferred until the end of the loop.

  4. One final caveat: In general, methods that begin with alloc and init (or a variation of the init method), will not generate autorelease objects, whereas all other methods, such as arrayWithObjects:count: will generate autorelease objects. One notable exception is the NSString class, which due to internal memory optimizations, does not conform to this rule. So, if you have any doubt, you can employ your own manual @autoreleasepool if you are repeatedly instantiating and releasing objects, and you are unsure as to whether the objects are autorelease objects or not. And, as always, profiling your app with the Allocations tool in Instruments is a good way of observing the app's high water mark. For an illustration of how these various techniques can impact the memory usage of your app, see https://stackoverflow.com/a/19842107/1271826.

Upvotes: 2

Chuck
Chuck

Reputation: 237100

The array was being retained by the autorelease pool. As described in Clang's Objective-C Literals documentation, the array literal syntax expands to a call to +[NSArray arrayWithObjects:count:], which creates an autoreleased array.

Upvotes: 1

Related Questions