Should I still copy/Block_copy the blocks under AR

2019-01-22 13:27发布

I've just stumbled over the following SO topic: Why should we copy blocks rather than retain? which has the following sentence:

However, as of iOS 6 they are treated as regular objects so you don't need to worry.

I was really confused by this assertion that is why I am asking: does this assertion really imply that Objective-C developers do not need to

@property (copy) blockProperties or

[^(...){...) {} copy]

to copy blocks and their contents from stack to heap anymore?

I hope the description I've made is clear.

Please, be verbose.


Similar questions

Under ARC, are Blocks automatically copied when assigned to an ivar directly?.

3条回答
Anthone
2楼-- · 2019-01-22 13:38

ARC will copy the block automatically. From clang's Objective-C Automatic Reference Counting documentation:

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.

So blocks used only as arguments to function or method calls may remain stack blocks, but otherwise anywhere that ARC retains the block it will copy the block. This is implemented by the compiler emitting a call to objc_retainBlock(), the implementation for which is:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

It is still a good idea to declare block properties as having copy semantics since a block assigned to a strong property will in fact be copied. Apple recommends this as well:

You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope. This isn’t something you need to worry about when using Automatic Reference Counting, as it will happen automatically, but it’s best practice for the property attribute to show the resultant behavior.

Note that since this copy-on-retain feature is provided by ARC it is dependent only on ARC or ARCLite availability and does not otherwise require a particular OS version or OS_OBJECT_USE_OBJC.

查看更多
Root(大扎)
3楼-- · 2019-01-22 13:57

Edit:

It turned out the examining the addresses of the "captured" variables are difficult to interpret, and not always appropriate to figure out whether a Block has been copied to the heap or still resides on the heap. Although the Specification of Blocks given here BLOCK IMPLEMENTATION SPECIFICATION, will sufficiently describe the facts, I try a completely different approach:

What is a Block anyway?

This is a summary of the official Specification BLOCK IMPLEMENTATION SPECIFICATION:

A Block exists of code (like a function) and a structure containing several peaces of data, flags and function pointers AND a variable length section for "captured variables".

Note that this structure is private and implementation defined.

A Block may be defined in function scope, where this structure is created in stack local memory, or it may be defined in global or static scope, where the structure is created in static storage.

A Block may "import" other Block references, other variables and __block modified variables.

When a Block references other variables, they will be imported:

  • A stack local (automatic) variable, will be "imported" by means of making a "const copy".

  • A __block modified variable will be imported by means of assigning a pointer the address of that variable enclosed in another structure.

  • Global variables will be simply referenced (not "imported").

If a variable will be imported, the "captured variable" lives in the aforementioned structure in the variable length section. That is, the "counterpart" of the automatic variable (which lives outside the block) has a storage within the block's structure.

Since this "captured variable" is read only, the compiler can apply a few optimizations: for example we really only need one instance of the captured variable on the heap, if we ever need a copy of the block.

The value of captured variables will be set when the block literal expression will be evaluated. This also implies, that the storage of the captured variables must be writable (that is, no code section).

The lifetime of captured variables is that of a function: each invocation of that block will require a new copy of these variables.

Captured variables in Blocks which live on the stack are destroyed when the program leaves the compound statement of the block.

Captured variables in Blocks which live on the heap are destroyed when the block will be destroyed.

According the Block API, versions 3.5:

Initially, when a block literal is created, this structure will exist on the stack:

When a Block literal expression is evaluated the stack based structure is initialized as follows:

  1. A static descriptor structure is declared and initialized as follows:

    a. The invoke function pointer is set to a function that takes the Block structure as its first argument and the rest of the arguments (if any) to the Block and executes the Block compound statement.

    b. The size field is set to the size of the following Block literal structure.

    c. The copy_helper and dispose_helper function pointers are set to respective helper functions if they are required by the Block literal.

  2. A stack (or global) Block literal data structure is created and initialized as follows:

    a. The isa field is set to the address of the external _NSConcreteStackBlock, which is a block of uninitialized memory supplied in libSystem, or _NSConcreteGlobalBlock if this is a static or file level Block literal.

    b. The flags field is set to zero unless there are variables imported into the Block that need helper functions for program level Block_copy() and Block_release() operations, in which case the (1<<25) flags bit is set.

Please note that this is for Block literals.

According the Objective-C Extensions to Blocks, the compiler will treat Blocks like objects.

Observations

Now, it's difficult to craft test code which proves these assertions. Thus, it seems better to use the debugger and set symbolic breakpoints at relevant functions

  • _Block_copy_internal and

  • malloc (which should be enabled only after the first breakpoint has been hit)

and then run suitable test code (like the snippets below) and examine what happens:

In the following code snippet, we create a Block literal and pass it through as parameter to a function which calls it:

typedef void (^block_t)(void);

void func(block_t block) {
    if (block) {
        block();
    }
}

void foo(int param)
{
    int x0 = param;
    func(^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    });
}  

The output is as follows:

Hello block 1
Address of auto y0: 0x7fff5fbff8dc
Address of captured x0: 0x7fff5fbff940

The address of the "captured" variable x0 strongly indicates that it lives on the stack.

We also have set a breakpoint at _Block_copy_internal - however, it won't be hit. This indicates, that the Block has not been copied onto the heap. Another proof can be made with Instruments, which doesn't show allocations in function foo.

Now, if we create and initialize a block variable, it seems, the Block data structure of the original Block literal - which is initially created on the stack, will be copied onto the heap:

int capture_me = 1;
dispatch_block_t block = ^{ int y = capture_me; };

enter image description here

This above copies the block which has been initially created on the stack. This may simply happen due to ARC and the fact, that we have a block literal on the right hand, and the block variable block on the left hand will be assigned the block literal - which results in a Block_copy operation. This makes Blocks appear much like normal Objective-C objects.

The Allocations traced with Instruments in this similar code below

void foo(int param)
{
    dispatch_queue_t queue = dispatch_queue_create("queue", 0);

    int x0 = param;
    dispatch_block_t block = ^{
        int y0 = x0;
        printf("Hello block 1\n");
        printf("Address of auto y0: %p\n", &y0);
        printf("Address of captured x0: %p\n", &x0);
    };

    block();
}    

show, that the Block will be indeed copied:

enter image description here

Noteworthy

  • When a block doesn't capture any variables, it is like a normal function. Then it makes sense to apply an optimization, where a Block_copy operation does actually nothing, since there is nothing to copy. clang implements this with making such blocks a "global block".

  • When sending copy to a block variable and assigning the result to another block variable, e.g.:

    dispatch_block_t block = ^{
        int y0 = capture_me;
    };
    dispatch_block_t otherBlock = [block copy];
    

    copy will be a quite cheap operation, since the block block has already allocated storage for the block's structure which can be shared. Thus, copy doesn't need to allocate storage again.

Back to the question

To answer the question if we need to explicitly copy a block in certain circumstances, for example:

@property (copy) block_t completion

[^{...} copy]

Well, once the block has been copied, no matter when, and will then be assigned the target variable (a Block variable) - we should be always safe without explicitly copying the block, since it is already in the heap.

In case of the block property, it should be safe if we would simply wright:

@property dispatch_block_t completion;

and then:

foo.completion = ^{ x = capture_me; ... };

This should be safe since assigning a block literal (which lives on the stack) the underlying block variable _completion, will copy the block onto the heap.

Nonetheless, I would still recommend to use attribute copy - since it is still suggested by the official documentation as best practice, and also supports older APIs where Blocks don't behave like normal Objective-C objects and where there is no ARC.

Existing system APIs will also take care of copying a Block if required:

dispatch_async(queue, ^{int x = capture_me;});

dispatch_async() will make the copy for us. So, we don't have to worry.

Other scenarios are more subtle:

dispatch_block_t block;
if (condition) {
    block = ^{ ... };
}
else {
    block = ^{ ... };
}
dispatch_sync(queue, block);

But actually, this is safe: The block literal will be copied and assigned the block variable block.

This example might look even scary:

int x0 = param;
NSArray* array = [NSArray arrayWithObject:^{
    int y0 = x0;
    printf("Hello block 1\n");
}];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
    block_t block = array[0];
    block();
});

But it seems, the Block literal will be correctly copied as an effect of assigning the argument (the Block) to the parameter (an id) in method arrayWithObject: which copies the Block literal onto the heap:

enter image description here

Additionally, NSArray will retain the object passed as parameter in method arrayWithObject:. This causes another call to Block_copy()however, since the Block is already onto the heap, this call does not allocate the storage.

This indicates, that a "retain" message sent to a Block literal somewhere in the implementation of arrayWithObject: would also indeed copy the Block.

So, when do we actually need to explicitly copy a Block?

A block literal - e.g. the expression:

^{...}

will create the block structure on the stack.

So, we actually need to make a copy only in cases where we need the Block on the heap and when this doesn't happen "automatically". However, there are virtually no scenarios where this is the case. Even in cases where we pass a Block as a parameter to a method which has no idea that it is a Block literal and requires a copy first (e.g.: arrayWithObject:), and possibly the receiver sends just a retain message to the object given in the parameter, the Block will be copied onto the heap first.

Examples where we might explicitly use copy, is where we do not assign a Block literal a block variable or an id, and thus ARC cannot figure out that it has to make copy of the block object or has to send "retain".

In this case, the expression

[^{...} copy];  

will yield an autoreleased Block whose structure resides on the heap.

查看更多
▲ chillily
4楼-- · 2019-01-22 14:01

Mine original answer was wrong. Edited answer is not an answer, but more of a "it's indeed a good question" thing.

Please, see Matt's answer for real references why stuff is the way it is.

After testing the following code on iOS7:

int stackVar;
void (^someBlock)() = ^(){};
NSLog(@"int: %x\nblock: %x,\ncopied: %x", (unsigned int)&stackVar, (unsigned int)someBlock, (unsigned int)[someBlock copy]);

I got this:

int: bfffda70
block: 9d9948
copied: 9d9948

Which pretty much means that blocks created and assigned into stack variables are actually already on a heap and copying them doesn't affect anything.

This, though, is not backed up by any official source, as they still state that blocks are created on stack on need to be copied "when passing down".


Part of the answer before I tested, stating which docs are contradicted by the example.

Document about transition to ARC states:

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.

And docs about blocks and properties says:

You should specify copy as the property attribute, because a block needs to be copied to keep track of its captured state outside of the original scope.

查看更多
登录 后发表回答