Why do NSBlocks have to be copied for storage in c

2019-01-24 20:52发布

问题:

- (void) addABlock 
{
void (^aBlock)(void) = ^() { [someObject doSomething]; };

[self.myMutableArray addObject: aBlock];  // Oops..

[self.myMutableArray addObject: [aBlock copy]];  // works fine
}

In the above simplified example I am seeing undefined behavior if the block copy is not performed. This case is specifically listed in apple's ARC transition guide.

The part that I do not understand is why I have to manually call copy. The block is created on the stack so a block_copy needs to be performed- that much is clear. NSArray does not call copy, but it should call retain on objects that get added. So why doesn't [NSBlock retain] simply call through to [NSBlock copy] ?

http://developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html

http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/

回答1:

Update

Despite what Apple documentation says:

Blocks “just work” when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more. You still need to use [^{} copy] when passing “down” the stack into arrayWithObjects: and other methods that do a retain.

it's no longer necessary to manually call copy on a block when adding it to a container. The lack of automatic copy in this case has been considered a compiler bug and fixed in llvm long time ago.

"We consider this to be a compiler bug, and it has been fixed for months in the open-source clang repository."

(John McCall, LLVM developer)

I personally tested this in Xcode 5, using the latest Apple LLVM 5.0 compiler.

- (NSArray *)blocks {
    NSMutableArray *array = [NSMutableArray array];
    for (NSInteger i = 0; i < 3; i++) {
        [array addObject:^{ return i; }];
    }
    return array;
}

- (void)test {
    for (NSInteger (^block)() in [self blocks]) {
        NSLog(@"%li", block());
    }
}

The above example correctly prints

0
1
2

under ARC and it crashes with EXC_BAD_ACCESS in MRC.

Note that this is - finally - coherent with the llvm documentation, which states

whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy

meaning that whenever ARC has to retain a pointer and this pointer happens to be a block-pointer type, Block_copy will be called instead of retain.


Original answer

The part that I do not understand is why I have to manually call copy.

Blocks are one of the few examples of Objective-C objects allocated on the stack (for performance reasons), so when you return from the method call you lose them, due to the tear down of the current stack frame.

Sending copy on a stack-block will call Block_copy on it and it will move it on the heap, allowing you to keep a valid reference to the block.

So why doesn't [NSBlock retain] simply call through to [NSBlock copy]

This would break the usual semantic of retain, which is supposed to return the object itself, with an incremented retain count. Since incrementing a retain count on an stack-block doesn't make any sense, calling retain on a stack-block doesn't have any effect.

Apple could have implemented it differently, as you suggest, but they preferred to stick as much as possible to the common contracts of memory management methods.

As a further reference on blocks, you may want to have a look at this great blog post by @bbum. It's pre-ARC but the majority of concepts hasn't changed.



回答2:

Under ARC, you no longer need to manually copy blocks in this case, or in most others. According to clang's ARC documentation on blocks

With the exception of retains done as part of initializing a __strong parameter variable or reading a __weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.

In other words, most of the time, retaining a block has the effect of a Block_copy as you were suggesting should be the case. In particular, when the block is added to a collection, it is copied! (More accurately, it is already on the heap.) Here is some sample code that demonstrates that this is the case.

#import <Foundation/Foundation.h>

int main(int argc, char *argv[]) 
{
    @autoreleasepool 
    {
        NSMutableArray *arr = [[NSMutableArray alloc] init];
        int counter = 0;
        int total = 5;
        for (int i = 0; i < total; i++) {
            void (^block)(void) = ^{
                NSLog(@"in this block, counter is %d", counter);
            };
            [arr addObject:block];
            counter += 1;
        }

        for (int i = 0; i < total; i++) {
            void (^block)(void) = arr[i];
            block();
        }
    }
}

If run on the latest Xcode (4.6.3 (4H1503)) with the default compiler (Apple LLVM compiler 4.2), the output for this will be

in this block, counter is 0
in this block, counter is 1
in this block, counter is 2
in this block, counter is 3
in this block, counter is 4

If these blocks were not copied to the heap, you would (probably--this is undefined behavior) see

in this block, counter is 4
in this block, counter is 4
in this block, counter is 4
in this block, counter is 4
in this block, counter is 4

because the pointers added to the array were all pointing to the stack-allocated (non-copied) block which--at the time that the blocks were executed--had captured a counter value of 4.

In particular, this is the same behavior that you will get if you disable ARC. Even if you call retain on the blocks before adding them to the array

[arr addObject:[block retain]];

you will still get the same ("broken") output, illustrating that this is an ARC behavior, not a retain behavior in general.

Note:

The two places where ARC's retain does not have the effect of Block_copy are

(1) retains done as part of initializing a __strong parameter variable

Immediately upon entering a function (or method), if an object is passed to that function (or method), that object will be retained (there is a strong reference to the object in the stack frame) and released when the function (or method) exits (the strong reference to the object is released).

This is just as true of blocks as it is true of any other object. This phrase in the clang documentation means that even though the block is retained in this case, the block will not be copied by this retain.

(2) retains done as part of reading a __weak variable

Similarly, when a __weak variable is read into a __strong variable, a strong reference to the object is created, and the object is retained.

This is the case for blocks as well. Blocks which are sent retain in this way (as a result of reading a __weak reference into a __strong reference), though, are not copied.

The cases where either of these two exceptions will cause you problems are rare. In general, you don't need to worry about copying your blocks.