5

I write some code use objective-c block, but the result confused me.

@interface MyTest : NSObject

@end

@implementation MyTest

- (void)test {
    NSArray *array = [self array1];  // ok
//    NSArray *array = [self array2];// crash
//    NSArray *array = [self array3];// ok again

    dispatch_block_t block0 = (dispatch_block_t)[array objectAtIndex:0];
    block0();

    dispatch_block_t block1 = (dispatch_block_t)[array objectAtIndex:1];
    block1();
}

- (NSArray *)array1 {
    int a = 10;
    NSMutableArray *array = [NSMutableArray array];
    [array addObject:^{
        NSLog(@"block0: a is %d", a);
    }];
    [array addObject:^{
        NSLog(@"block1: a is %d", a);
    }];
    return array;
}

- (NSArray *)array2 {
    int a = 10;

    return [NSArray arrayWithObjects:^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }, nil];
}

- (NSArray *)array3 {
    int a = 10;

    return [NSArray arrayWithObjects:^{
        NSLog(@"block0: a is %d", a);
    },[^{
        NSLog(@"block0: a is %d", a);
    } copy], nil];
}
@end

I am confused about:

  1. why array2 crash? what's the REAL difference between array1 and array2 ?
  2. I read some article said block copy will move a block from stack to heap, but in method array1, I do not copy it and it still works. in array3 I just copy the second block it become ok. why ?
  3. where I must use copy when I use block?

BTW, I run the code in Xcode 4.6, under ARC. Thanks

Shaik Riyaz
  • 11,204
  • 7
  • 53
  • 70
zhaoyk10
  • 67
  • 1
  • 6

3 Answers3

4

You seem to have found a case of type loss in relation to blocks which the compiler does not handle. But we need to start at the beginning...

The following relates to the use of blocks under ARC. Other scenarios (MRC, GC) are not considered.

That some blocks are created on the stack rather than the heap is an optimisation that could technically be implemented in such a way that programmers never need to be aware of it. However when blocks were first introduced the decision was made that the optimisation would not be transparent to the user, hence the introduction of blockCopy(). Since that time both the specification and the compiler have evolved (and the compiler actually goes beyond the spec), and blockCopy() is not (by the specification) needed it places it used to be, and may not (as the compiler may exceed the spec) be needed in others.

How can the optimisation be implemented transparently?

Consider:

  1. The compiler knows when it creates a stack allocated block; and
  2. The compiler knows when it assigns such a block to another variable; so
  3. Can the compiler figure out for each assignment whether the block needs to be moved to the heap?

The trivial answer is "yes" - move to the heap on any assignment. But that would negate the whole purpose of the optimisation - create a stack block, pass it to another method, which involves and assignment to the parameter...

The easy answer is "don't try" - introduce blockCopy() and let the programmer figure it out.

The better answer is "yes" - but do it smartly. In pseudo-code the cases are:

// stack allocated block in "a", consider assignment "b = a"
if ( b has a longer lifetime than a )
{
   // case 1: assigning "up" the stack, to a global, into the heap
   // a will die before b so we need to copy
   b = heap copy of a;
}
else
{
   if (b has a block type)
   {
      // case 2: assigning "down" the stack - the raison d'être for this optimisation
      // b has shorter life (nested) lifetime and is explicitly typed as a block so
      // can accept a stack allocated block (which will in turn be handled by this
      // algorithm when it is used)
      b = a;
   }
   else
   {
      // case 3: type loss - e.g. b has type id
      // as the fact that the value is a block is being lost (in a static sense)
      // the block must be moved to the heap
      b = heap copy of a;
   }
}

At the introduction of blocks cases 1 & 3 required the manual insertion of blockCopy(), and case 2 was where the optimisation paid off.

However as explain in an earlier answer the specification now covers case 1, while the compiler appeared to cover case 3 but no documentation confirming that was known.

(BTW if you follow that link you will see it contains a link to an older question on this topic. The case described there is now handled automatically, it is an example of case 1 above.)

Phew, got all that? Let's get back to the examples in the question:

  • array1, array3 and array4 are all examples of case 3 where there is type loss. They are also the scenario tested in the previous question and found to be handled by the current compiler. That they work is not an accident or luck, the compiler inserts the required block copies explicitly. However I don't know this is officially documented anywhere.
  • array2 is also an example of case 3 where there is type loss, but it is a variation not tested in the previous question - type loss by passing as a part of a variable argument list. This case does not appear to be handled by the current compiler. So now we have a clue as to why handling of case 3 is not documented - the handling is not complete.

Note that, as mentioned previously, it is possible to test what your compiler does - you can even incorporate some simple tests in your code to immediately abort an application if the tests fail. So you can, if you wish, write code based on what you know the compiler currently handles automatically (so far everything considered accept variadic functions) and which will abort your code should you update the compiler and the replacement lacks the support.

Hope this was helpful and makes sense!

Community
  • 1
  • 1
CRD
  • 52,522
  • 5
  • 70
  • 86
1

All three of these crash for me (although I suspect the lack of a copy on the first element of array3 is probably an oversight.) A block has to be copied if you want it to outlive the scope in which it was created. Unless you specifically know that a method copies the object you pass into it, you need to copy it yourself.

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • You should always copy the block if passing to any method or function that operated asynchronously unless that method or function is explicit about copying said block. – bbum Jun 08 '13 at 17:25
  • Under what conditions did you run the code? I get the same results as the OP when used as part of a simple OSX program under OSX 10.7.5. – rmaddy Jun 08 '13 at 17:35
  • Whether or not any of them happen to work in a given situation, they're still wrong since `arrayWithObjects:` and `addObject:` don't specifically say they copy their parameters. – ipmcc Jun 08 '13 at 17:40
  • That may be true but I'm still curious under what conditions you ran the code. For my own education I'd like to know what you did different than the OP and myself. – rmaddy Jun 08 '13 at 18:03
  • I tried it both under ARC and not under ARC. In my answer, I was referring to the no-ARC case. ARC's implementation is effectively opaque. When I want definitive, deterministic answers, I turn off ARC. – ipmcc Jun 08 '13 at 18:08
  • FWIW, I suspect that ARC is copying the first argument because it's type is specifically called out as `id` in the method prototype. It can't know the types of the following variadic parameters (i.e. think of `printf` where the variadic arguments can be of arbitrary type) so it doesn't copy them. – ipmcc Jun 08 '13 at 18:11
0

I tried a fourth case that also works just fine:

- (NSArray *)array4 {
    int a = 10;

    return @[ ^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }
             ];
}

Of course this is the same as:

- (NSArray *)array4 {
    int a = 10;

    id blocks[] = { ^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }
    };
    NSUInteger count = sizeof(blocks) / sizeof(id);

    return [NSArray arrayWithObjects:blocks count:count];
}

So the only issue is with "array2". The key point with that implementation is that you are calling the arrayWithObject: method which takes a variable number of arguments.

It seems that only the first (named) argument is properly copied. None of the variable arguments are copied. If you add a third block the problem still arises on the 2nd block. Only the first block is copied.

So it seems that using the blocks with the variable argument constructor, only the first named argument is actually copied. None of the variable arguments are copied.

In all other approaches to creating the array, each block is copied.

BTW - I ran your code and my additions using Xcode 4.6.2 using a simple OS X app under Lion (10.7.5) using ARC. I get identical results when the same code is used in an iOS 6.1 app.

rmaddy
  • 314,917
  • 42
  • 532
  • 579