Block
Introduction to the
This paper mainly answers the following questions:
- What is a Block?
- What are the different types of occurrence scenarios of blocks? Where are they stored?
- How are captured variables stored?
- How to solve the circular reference problem?
The succinct answer:
- A Block, also known as an anonymous function, is essentially an ObjC object with a pointer to a specific function implementation in its structure.
- It has three types, which are stored in static data area, stack area, and heap area respectively.
- Intercepted variables are copied directly into the Block structure, or captured in its pointer.
- The weak and __block modifiers are generally used to solve the circular reference problem.
Next, elaborate on each answer.
What is a Block?
In one sentence: an anonymous function that can harvest local variables.
But what about the internal implementation?
Let’s write a simple Block
#import <Foundation/Foundation.h>
int main() {
void (^blk)(void) = ^{ printf("BLOCK\n"); };
blk();
return 0;
}
Copy the code
Use clang to convert to c++
clang -rewrite-objc xx.m
Copy the code
The key code to get the result is as follows:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};^{printf("BLOCK\n",); } to the following code
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("BLOCK\n");
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {0.sizeof(struct __main_block_impl_0)};
int main(a) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
Copy the code
After removing the constructor, the Block becomes:
// Block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
// __block_impl declaration
struct __block_impl {
void *isa;
int Flags; // Bit reserved to indicate some additional block information
int Reserved; // Keep variables
void *FuncPtr;
};
// __main_block_desc_0 declaration
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = {0.sizeof(struct __main_block_impl_0)};
Copy the code
If you look at ObjC object declarations, it’s not hard to see that blocks are essentially ObjC objects.
Because it also has that iconic ISA pointer.
// Objective-C object declaration
// id is the pointer type of the objc_object structure
typedef struct objc_object {
Class isa;
} *id;
Class is the pointer type of the objc_class structure
typedef struct objc_class *Class;
struct objc_class {
Class *isa;
};
Copy the code
The three types of blocks
The isa of a Block refers to one of these three classes, and the name of the class indicates the location of each class.
__NSStackBlock__
Stored on the stack__NSGlobalBlock__
Stored in the static data area__NSMallocBlock__
The heap
The scenarios for each type are as follows:
int e = 3;
void (^block)() = ^{
printf("%c\n", e);
};
NSLog(@"%@", [block class]); // __NSMallocBlock__
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__
NSLog(@"%@", [^{ int a = 4; printf("%c\n", a); } class]); // __NSGlobalBlock__
Copy the code
Simple to understand, the unintercepted variable is __NSGlobalBlock__.
If a variable is intercepted, it is __NSStackBlock__, but it is easily copied to the heap.
For example:
- When copy is called on a Block object.
- When a Block is returned as a return value. (If ARC environment is required)
- When a Block is assigned to a strong object.
- When passing a Block in a Cocoa framework method with usingBlock in the method name, or in an API of GCD.
Sometimes you also need to manually add a copy method to add blocks to the heap.
Depending on the Block, copy has different effects:
Block class | The configuration storage domain of the copy source | Print effect |
---|---|---|
_NSConcreteStackBlock | The stack | Copy from stack to heap |
_NSConcreteGlobalBlock | The data area of the program | Nothing |
_NSConcreteMallocBlock | The heap | Reference count increment |
How are captured variables stored?
The intercepted variables are added to the __main_block_IMPL_0 structure.
Something like this:
__block_impl |
---|
isa * |
Flags |
Reserved |
FuncPtr |
Intercepted variable 1 |
Intercepted variable 2 |
. |
Converted code:
int main(a) {
int count = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count; // The automatic variable used is appended to the structure as a member variable
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("BLOCK%d\n", count);
}
Copy the code
__block specifier
After adding __block, if the data changes, executing block also prints the latest data:
No __block is added
int n = 3; void (^block2)() = ^{ NSLog(@"%d", n); }; n = 4; block2(); / / 3Copy the code
Added a __block that captures Pointers to variables.
So the value changes, and when the block executes, you see the changed data.
__block int n = 3; void (^block2)() = ^{ NSLog(@"%d", n); }; n = 4; block2(); / / 4Copy the code
Look at the transformed code for details.
The added __Block_byref_count_0 structure captures the pointer to the variable (__forwarding).
int main() { __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 3}; void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; } struct __Block_byref_count_0 {void *__isa; struct __Block_byref_count_0 {void *__isa; __Block_byref_count_0 *__forwarding; int __flags; int __size; int count; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_count_0 *count; // by ref ... } static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_count_0 *count = __cself->count; // bound by ref (count->__forwarding->count) = 2; printf("BLOCK%d\n", (count->__forwarding->count)); }Copy the code
__forwarding
__forwarding is the key implementation when __block is added.
(For narrative purposes, the __forwarding variable is replaced by f below)
When a __block variable is on the stack, f points to a pointer to itself.
When the __block variable is copied to the heap, f is copied to the heap, assuming FM.
F on the stack becomes a pointer to a __block variable on the heap, and FM points to a pointer to the heap itself. That is, f and FM point to the same pointer.
So using the __forwarding variable, you can access the same variable in the following situations:
- Used in the Block,
- Outside the Block,
- Variables are on the stack or heap
How to solve the Block circular reference problem
The use of blocks tends to cause circular references, which are largely avoided by the __weak and __block modifiers.
__weak is more common and will not be repeated.
Main advantages of using __block:
You are free to control when variables are converted to nil
Main disadvantages of using __block:
You have to execute the Block, and inside the Block, set the variable to nil in order to avoid circular references.
doubt
The following is the author in sorting out this article, there are still doubts, if readers know, trouble to inform.
Why does the following code print differently?
int e = 3;
NSLog(@"%@", ^{ printf("%c\n", e); }); // <__NSMallocBlock__: 0x10046b050>
NSLog(@"%@", [^{ printf("%c\n", e); } class]); // __NSStackBlock__
Copy the code
The following analysis of assembly code ideas from ha is my 26593
As you can see, the previous line of code calls objc_retainBlock(), which doesn’t see the action.
As explained by objc_retainBlock — Clang 12 Documentation, this function copies blocks on the stack to the heap.
As for why to call it, there have been several opinions in the comments section, but I have not found evidence 😂.
So why copy blocks on the stack to the heap?
The personal understanding is that it is “needed” by objects on the heap, that is, it needs control over when it is destroyed, so it is copied to the heap for management purposes.
Thank you
Block quiz
Apple Official Documentation
Donqiao’s blog on objective-C Block implementation
Block Programming Topics
Block Implementation Specification — Clang 12 documentation
Where did NSStackBlock go under ARC? – Jane
Weak-strong in Block