Originally written by Matt Galloway
The original address: www.galloway.me.uk/2013/05/a-l…
This article is long overdue. I worked on the first draft for a few months, but I was too busy working on my book Effective Objective-C 2.0 to finish this article. But now THAT I’m done, let’s take a look.
Following part 1 and Part 2’s exploration of the internals of blocks, this article delves more deeply into what happens when blocks are copied. You’ve probably heard “Blocks start on the stack,” “you need to copy if you want to save blocks for later applications.” But why? What actually happens when you copy? I’ve always wondered what the mechanism of block copy is. For example: What does the block do with the captured value? In this article, I will explore.
The results of the research so far
From Part 1 and Part 2, we conclude that the structure of blocks in memory is roughly as follows:
In Part 2 we found that this structure is created on the stack when the block is initially referenced. Once on the stack, that chunk of memory can be reset after the block’s enclosing scope. So what do you do later when you want to use blocks? Yes, you can copy it. Because a block is an Objective-C object, you can either call the Block_copy method or send an Objective-C copy of the message directly like the block. Only Block_copy() is called.
So, what’s more valuable than exploring what Block_copy does.
Block_copy()
First, we need to look at the block. h file. Here’s the definition:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
void *_Block_copy(const void *arg);
Copy the code
Therefore, Block_copy() is just a #define definition that forces the passed argument to const void * and passes the argument to _Block_copy(). The following is the prototype of _Block_copy(), which is implemented in Runtime.c:
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
Copy the code
So you just call _Block_copy_internal() and pass the block itself and WANTS_ONE. To understand what this means, you need to look at his implementation. This is also in Runtime.c. Here is the function that removes some of the irrelevant content (mainly the garbage collector processing related content).
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
/ / 1
if(! arg)return NULL;
/ / 2
aBlock = (struct Block_layout *)arg;
/ / 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
/ / 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
/ / 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if(! result)return (void *)0;
/ / 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
/ / 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
/ / 8
result->isa = _NSConcreteMallocBlock;
/ / 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
Copy the code
Here is a list of what this method does:
1. If the arG is passed NULL, return NULL. This also makes it safe for functions to pass a NULLblock.
2. If the argument is passed to a pointer of type struct Block_layout. You might remember what these were in Part 1. This is the internal data structure that makes up a block, including Pointers to the block’s implementation functions and various metadata bits.
3. If BLOCK_NEEDS_FREE is included in the block flags, it is a heap block (as you will see in a moment). In this case, all you need to do is reference count +1 and return the same block.
4. If it is a global block(remember from Part 1), there is nothing to do but return the same block. This is because a global block is actually a singleton.
5. If we reach comment 5, the block must be a block that is occupying space. In this case, the block needs to be copied to the heap. This part is interesting. First, use malloc() to create a block of memory of the required size. If the creation fails, return NULL, otherwise proceed.
6. In this case, the memmove() function is used to bitwise copy the currently allocated blocks on the stack to the memory space we allocated for the heap blocks in the previous step. This ensures that all raw data such as block descriptor can be copied.
7. Next, block flags are updated. The first line ensures that the reference count is set to 0. The comment indicates that this step is not necessary, presumably because the reference count should already be 0 at this point. I guess this line is left in there to prevent the application from having a non-zero count in some cases. The next line sets the BLOCK_NEEDS_FREE identifier. This indicates that this is a block on the heap, and once the application count is reduced to zero, the memory holding the block needs to be freed. This line | 1 (1) and the operation, will block the reference count to 1.
8. Here, the isa pointer to block is set to _NSConcreteMallocBlock, which means that this isa heap block.
9. Finally, if the block has a copy helper function, this function will be created. The compiler creates a copy helper if necessary (such as when a block is needed to capture an object). In the case of a block capturing object, the Copy Helper function needs to retain the captured object.
Well! This is so clever! Now you know how a block is copied. But that’s only half the job, right? How does a block get released?
Block_release()
The other half of Block_copy() is Block_release(). Again, Block_release() is actually the following macro definition:
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
Copy the code
Just like Block_copy(), Block_release() calls a function after converting arguments for us. This helps the developer, the developer doesn’t have to do the casting themselves.
Let’s take a look at the _Block_release() function (reconfigured for clearer logic and removed garbage collection) :
void _Block_release(void *arg) {
/ / 1
struct Block_layout *aBlock = (struct Block_layout *)arg;
if(! aBlock)return;
/ / 2
int32_t newCount;
newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
/ / 3
if (newCount > 0) return;
/ / 4
if (aBlock->flags & BLOCK_NEEDS_FREE) {
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
_Block_deallocator(aBlock);
}
/ / 5
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
;
}
/ / 6
else {
printf("Block_release called upon a stack Block: %p, ignored\n", (void*)aBlock); }}Copy the code
1. First, the argument is converted to a pointer of type struct Block_layout, as it is of that type. If the argument is passed NULL, we return early to ensure that NULL is also passed safely.
2. The identifier part of the block that specifies the reference count (think of Block_copy() where the identifier is set to the reference count of 1) is reduced.
3. If the new reference count is greater than zero, it indicates that there are still objects that reference the block, so the block cannot be freed.
4. Conversely, if the flags contain BLOCK_NEEDS_FREE, and the heap block and reference count are zero, the block is freed. First, the Block’s Dispose Helper is called. This is the opposite of copy Helper for blocks. This function does the opposite, such as releasing any captured objects. Eventually, the block is destroyed using the _Block_deallocator function. If you look in [runtime.c]([]()), you will see that this is a pointer to the free function that freed malloc’s memory.
5. If we succeed and it is a global block, do nothing.
6. If we get to comment 6, something will happen because a stack block view will be released and a log will be printed to alert the developer. In fact, you shouldn’t see this message.
That’s it. There’s nothing more.
The next step
My journey to spy on the Block ends here. Some of it is in my book Effective Objective-C 2.0. That information is about how to use blocks effectively. But there’s still plenty of interesting in-depth material if you’re interested.
Related series
- A Look Inside Blocks: Episode 1
- A Look Inside Blocks: Episode 2
- A Look Inside Blocks Episode 3 (Block_copy)