Understanding Automatic Reference Counting (ARC)


ARC was introduced in XCode 4.2. Immediately, the technology became shrouded in an aura of mystery. Experts proclaimed that there is no mystery. The technology is completely deterministic. It may be deterministic, but, exactly how the system behaves under different situations is not very well documented. In this article, I will try to explain the basics through examples.

Consider the code:

@interface Address : NSObject {
    @public
    NSString *city;
}
@end

@implementation Address
- (Address*) init: (NSString*) c {
    city = c;

    return self;
}

- (void) dealloc {
    NSLog(@"Destroying address: %@", city);
}
@end

@interface Customer : NSObject {
    NSString *name;
    Address *addr;
}
@end

@implementation Customer
- (Customer*) init: (NSString*) n withAddress: (Address*) a {
    //Note 1: Automatically retain on assignment
    name = n;
    addr = a;

    return self;
}

- (void) dealloc {
    NSLog(@"Destroying: %@", name);
    //Note 2: All member variables are released automatically
}

@end

Customer* objectReturnTest() {
    NSString * n = [[NSString alloc] initWithString: @"Billy Bob"];
    Address * a = [[Address alloc] init: @"New York City"];       

    Customer *c = [[Customer alloc] init: n withAddress: a];

    //Note 3: ARC will put the returned object in autorelease pool.
    return c;
}

A couple of basic things to note here. As “Note 1”  says, when an object is assigned to a variable, a call to retain is made automatically. This increments the reference count. As “Note 2” says, when an object is destroyed, all member variable objects are released for you. You no longer have to do that from the dealloc method.

Finally, when a method returns a newly created object, ARC will put the returned object in an autorelease pool. This is stated in “Note 3”.

Now, let’s use the code.

int main (int argc, const char * argv[])
{
    NSString * n = [[NSString alloc] initWithString: @"Johnny Walker"];
    Address * a = [[Address alloc] init: @"Miami"];
    Customer *c = [[Customer alloc] init: n withAddress: a];

    NSLog(@"Before force release");
    c = nil; //Force a release
    NSLog(@"After force release");

    @autoreleasepool {

        Customer *c2 = objectReturnTest();

    }
    NSLog(@"After autorelease pool block.");

    return 0;
}

The log output from this code will be:

Before force release

Destroying: Johnny Walker

After force release

Destroying: Billy Bob

Destroying address: New York City

After autorelease pool block.

Destroying address: Miami

A couple of things to note here. See how force release works. We set a variable to nil. ARC immediately releases the reference count. This causes the Customer object “Johnny Walker” to get destroyed. But, the member Address object “Miami” doesn’t get destroyed. That is because the Address variable a still has a reference to that object. This Address object gets destroyed at the very end of the main method.

The object return test works as expected. Customer “Billy Bob” is put in auto release pool. At the end of the @autoreleasepool block, the pool is drained and the object is released.

Finally, we will now test for another common scenario. Objects may be created within a method that are not returned. Exactly when are they released and destroyed. The answer is, at the end of the method. Here is an example:

void temporaryObjectTest() {
    Address * a1 = [[Address alloc] init: @"Los Angeles"];
    Address * a2 = [[Address alloc] init: @"Seattle"];
}

We can call the method as follows:

NSLog(@"Before method call");
temporaryObjectTest();
NSLog(@"After method call");

The log output will be:

Before method call

Destroying address: Seattle

Destroying address: Los Angeles

After method call

These temporary objects are not put in autorelease pool. They are simply released at the end of the method. Only a new object returned from a method is autoreleased.

  1. #1 by Barry-John Theobald on October 23, 2012 - 5:35 pm

    In your main function, after creating your customer object you can assign nil to both a and n (since these are retained by c). Once you assign nil to c, both a and n will be released, which I assume is the behaviour you are expecting.

    The reason that a is not released when c is assigned nil is because the retain count is still 1.

  2. #2 by Bibhas Bhattacharya on October 23, 2012 - 6:15 pm

    Hi Barry, Thanks for pointing it out. I will fix the post.

(will not be published)

*