Matt Galloway

My home on the 'net.

A look under ARC's hood - Episode 2

Following on from my first post about looking at how ARC works under the hood I thought I would share another little snippet that I found interesting. This time I was wondering what happened when you pulled an object out of an array and returned it from a method. Pre-ARC, you would retain the object then return it autoreleased. With ARC we can get rid of those memory management calls but it just feels wrong. So I decided to check ARC was doing the right thing.

Consider this class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <Foundation/Foundation.h>

@interface ClassA : NSObject
@property (nonatomic, strong) NSMutableArray *array;
@end

@implementation ClassA

@synthesize array;

- (id)popObject {
    id lastObject = [array lastObject];
    if (lastObject) {
        [array removeLastObject];
    }
    return lastObject;
}

@end

In non-ARC land, the call to removeLastObject would release the object that was in the array and then if that was the last reference to it, it would dealloc the object meaning that we return a dead object. So we would retain lastObject and then return it with an autorelease.

But this scared me not doing this, even though I knew full well that ARC should be doing its job. I think I thought this because naively I thought that ARC would parse the method line by line. If it did then I thought it might not necessarily add in a retain because when we get a reference to the last object, ARC doesn’t know it needs to add a retain because well, why would it necessarily have to?

That’s where I was wrong. Obviously what ARC does is it will add in a retain once we get a reference to that object and then when that variable goes out of scope it adds a release or in our case, because we are returning it and the method name does not start with new or copy, it autoreleases it.

Let’s see what that code compiled to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    .thumb_func     "-[ClassA popObject]"
"-[ClassA popObject]":
    push    {r4, r5, r6, r7, lr}
    movw    r6, :lower16:(_OBJC_IVAR_$_ClassA.array-(LPC0_0+4))
    mov     r4, r0
    movt    r6, :upper16:(_OBJC_IVAR_$_ClassA.array-(LPC0_0+4))
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_1+4))
LPC0_0:
    add     r6, pc
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC0_1+4))
LPC0_1:
    add     r1, pc
    add     r7, sp, #12
    ldr     r0, [r6]
    ldr     r1, [r1]
    ldr     r0, [r4, r0]
    blx     _objc_msgSend
    @ InlineAsm Start
    mov     r7, r7          @ marker for objc_retainAutoreleaseReturnValue
    @ InlineAsm End
    blx     _objc_retainAutoreleasedReturnValue
    mov     r5, r0
    cbz     r5, LBB0_2
    movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC0_2+4))
    movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_2-(LPC0_2+4))
    ldr     r0, [r6]
LPC0_2:
    add     r1, pc
    ldr     r1, [r1]
    ldr     r0, [r4, r0]
    blx     _objc_msgSend
LBB0_2:
    mov     r0, r5
    blx     _objc_autoreleaseReturnValue
    pop     {r4, r5, r6, r7, pc}

Well, there we go. ARC has done a fine job for us. What it’s actually done is added a call to objc_retainAutoreleaseReturnValue which means it has noticed it needs to retain a value that was returned autoreleased which must be an ARC optimisation that will just pull the object once from the autorelease pool rather than actually performing a retain. Then at the end of the method it calls objc_autoreleaseReturnValue which must do the work of autoreleasing the value we’re returning.

This is just another example of how to look at what ARC is doing under the hood. The more I use ARC, the more I realise how useful it is. It makes code less prone to memory management errors and allows for optimisations such as the one shown here of retaining an autoreleased return value.

Comments