Search code examples
iosobjective-cblockobjective-c-blocks

Object lifecycle with a method receiving a block, under ARC


Right now I have something like this:

- (void)viewDidLoad
{
    MyObject *myObject = nil;
    @autoreleasepool
    {
        myObject = [[MyObject alloc] init];

        [myObject doSomethingWithBlock:^{
            NSLog(@"Something Happened");
        }];
    }

    NSLog(@"End of method");
}

And the doSomethingWithBlock: has the following:

- (void)doSomethingWithBlock:(void(^)())aBlock
{
    [self performSelector:@selector(something:) withObject:aBlock afterDelay:4.0f];
}

And the something:

- (void)something:(void(^)())aBlock {
    aBlock();
 }

I understand that the block is being passed between stacks frames, so it's kept alive until it actually is executed and it is disposed. But why when the "End of method" is called does myObjct still exists? I know that I am not referencing the object from within the block, so shouldn't it be released within the autorelease pool? Or is this a compiler detail (if it should actually release it there, or after the method returns), and I shouldn't care about it?


Someone pointed out that performSelector will retain the target so I used this instead:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    aBlock();
});

Solution

  • It is not clear to me whether the question is about the lifetime of the block, as per the title, or the lifetime of the object referenced by myObject, so we'll cover both.

    First, the stack allocation of blocks in an optimisation and something programmers should not really need to be aware of. Unfortunately when blocks were first introduced the compiler was not smart enough to handle this optimisation completely automatically, hence the need to know about Block_copy() etc. But the compiler is now much smarter and the programmer can pretty much forget about stack allocation of blocks.

    This answer applies to Xcode 5.0.2/Clang 4.2. Use a different (earlier) version and YMMV.

    Second, the block:

    ^{ NSLog(@"Something Happened"); }
    

    is not actually stack allocated at all. As it references no values from the environment it is statically allocated and never needs to be placed on the stack or moved to the heap - another optimisation. To get a stack allocated block just change it to:

    ^{ NSLog(@"Something Happened: %p", self); }
    

    which will be stack allocated (and don't worry about possible retain cycles, that's another topic...)

    With that out of the way, let's look at the lifetimes:

    The object:

    The variable myObject has a lifetime of at most the call to viewDidLoad, the variable is created on entry to the method and destroyed on exit, if precise lifetime semantics are in force, or earlier if not needed and precise lifetime semantics are not in force - the latter is the default allowing the compiler to optimise storage use (independent of ARC). By default local variables have a strong ownership qualifier, so an ownership interest is asserted for any reference stored in one. ARC will release that ownership interest when another reference is stored in the variable or when the variable itself is destroyed. The latter will happen in this case and the variable is destroyed at some time after the last use of it and the end of the method - depending on how the compiler does its optimisations.

    All this means that the object referenced by myObject may or may not be still around at your call to NSLog(). In the case of using performSelector:withObject:afterDelay: that call will assert ownership over the object until after the selector has been executed -= so the object will live. In the case of using dispatch_after the object is not required and so will not stay alive.

    The block:

    As mentioned above as written the block is statically allocated so lifetime is easy - it always exists. More interesting is modifying it as above to make it stack allocated.

    If the block is stack allocated it will exist in the stack frame of the viewDidLoad call and unless moved or copied to the heap it will disappear when that call returns.

    The call to doSomethingWithBlock: will be passed a reference to this stack allocated block. This is possible as doSomethingWithBlock: expects a block and code within it can move it to the heap if needed as it knows it is a block.

    Now it gets interesting. Within doSomethingWithBlock: in the performSelector:withObject:afterDelay: case the block is passed as the withObject: parameter which is of type id - which means the called code expects a standard Objective-C object and those live on the heap. This is a case of type information loss, the caller has a block, the callee only sees an id. So at this point the compiler inserts code to copy the block to the heap and passes that new heap block to performSelector:withObject:afterDelay: - which treats it as it would any other object. [Older compilers did not always do this, the programmer had to know that type information loss was about to occur and manually insert code to move the block to the heap.]

    In the case of dispatch_after, the function argument is block typed so the compiler just passes a stack allocated block on. However the dispatch_after function copies the block to the heap, as per its documentation, and there is no problem.

    So in either case a stack allocated block is copied to the heap before viewDidLoad finishes and its stack frame is destroyed taking with it the stack allocated block.

    HTH.