Implementation principle of Block
Block source code analysis
Implement a simple block call in the main function
int main(){
int num = 0;
void (^block)(void) = ^{
NSLog(@"num: %d", num);
};
block();
return 0;
}
Copy the code
Use xcrun-sdk iphoneos clang-arch arm64-rewrite-objc main.m to convert main.m to main. CPP to view the source code implementation of block Here is the c++ code in the main. CPP file related to the block implementation
// Block object class
struct __block_impl {
void *isa; // The isa pointer points to the specific type of block
int Flags;
int Reserved;
void *FuncPtr; // Function pointer
};
// Wrap the concrete block object and the captured context variable into an object
struct __main_block_impl_0 {
struct __block_impl impl; // Concrete block implementation
struct __main_block_desc_0* Desc;
int num; // Variables captured from the context
// constructor
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock; // Block type
impl.Flags = flags;
impl.FuncPtr = fp; // Function pointerDesc = desc; }};// Block function implementation
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString*)&__NSConstantStringImpl__var_folders_z6_mpw8gfyn44vft3h3h82bz3hc0000gn_T_main_ad7ede_mi_0, num);
}
// Block description
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // The memory space required by the block
} __main_block_desc_0_DATA = { 0.sizeof(struct __main_block_impl_0)};
/ / the main function
int main(a){
int num = 0;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
Copy the code
The key analysis
Looking at the code above, we can see several important objects and several important members within them:
__block_impl
-
Isa pointer: Looking at the __block_impl internal structure, we can see that the first isa pointer is the same as the ISA in the OC object. The ISA in the block also refers to its specific type. The reason a block is considered an object is because it contains an ISA pointer. The specific types to which isa Pointers point will be examined below.
-
FuncPtr pointer: Points to a specific function implementation in a block.
__main_block_impl_0
- __block_impl object: A concrete block object
- __main_block_DESc_0: The description object contains information about the size of __main_block_desc_0 in memory
- Num: Captured external variable num
Analysis: __main_block_IMPL_0 internal structure, found that __main_block_IMPL_0 object is actually the specific block object and captured context variables are encapsulated, so that the block call to access the captured variables.
We can actually think of __main_block_IMPL_0 as a __block_impl subclass. I’ll explain why.
Analysis of main function
So in the main function above we’re doing two things: creating a block and calling a block. Looking at the implementation of main, we can see that when we create a block, we actually create a __main_block_IMPL_0 structure object. We actually call a block by calling a function pointer to a __block_impl object and passing in a __main_block_IMPL_0 object as an argument to the function.
Observe the invocation of a block
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
Copy the code
As you can see, the call converts the block object to an object of type __block_impl and then calls the function pointer in the __block_impl object, passing in the block object. __main_block_IMPL_0 can be thought of as a __block_impl subclass.
There is a question as to why block objects can access function Pointers in __block_impl objects, since __main_block_IMPL_0 bin is not actually a __block_impl subclass. How does it do it?
This is the memory layout of the interior of the object. For example, in an array: int list[10], how do we access the first and tenth elements? The answer is by subscript list[0]; The list [9]. But why are subscripts accessed correctly? Because the memory distribution of the array elements is continuous, we can access the data at the offset position correctly by using the offset pointer. List [9] actually offsets the list pointer by 9 and accesses its 10th element. The memory address of the element in the first position is the same as the content address of the array. We can get the contents of the first position directly by *list. Just as arrays are distributed, objects are distributed in memory. And the __block_impl member is the first position in the __main_block_IMPL_0 structure, so the __main_block_IMPL_0 pointer points to the same address as the __block_impl pointer, This is why the __main_block_IMPL_0 address has correct access to the FuncPtr function pointer.
Three block types
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock; // Block type
impl.Flags = flags;
impl.FuncPtr = fp; // Function pointer
Desc = desc;
}
Copy the code
Look at the __main_block_IMPL_0 constructor, which points the ISA pointer to the IMPl object to the _NSConcreteStackBlock object. This says that this block type is a stack block. In addition to the stack type block, there are also global types and heap types, depending on what type of object ISA points to.
int main() {
//1.
void (^global_block)(void) = ^{
NSLog(@"global_block");
};
NSLog(@"global_block: %@ -> %@ -> %@ -> %@",[global_block class], [[global_block class] superclass], [[[global_block class] superclass] superclass], [[[[[global_block class] superclass] superclass] superclass] superclass]);
//2.
__block int age = 1;
void (^stack_block)(void) = ^{
NSLog(@"stack_block %d", age++);
};
NSLog(@"stack_block: %@ -> %@ -> %@ -> %@",[stack_block class], [[stack_block class] superclass], [[[stack_block class] superclass] superclass], [[[[[stack_block class] superclass] superclass] superclass] superclass]);
//3.
void (^malloc_block)(void) = [stack_block copy];
NSLog(@"malloc_block: %@ -> %@ -> %@ -> %@",[malloc_block class], [[malloc_block class] superclass], [[[malloc_block class] superclass] superclass], [[[[[malloc_block class] superclass] superclass] superclass] superclass]);
}
Copy the code
The output
Global_block: __NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject stack_block: __NSStackBlock__ -> __NSStackBlock -> NSBlock -> NSObject malloc_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject __NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject stack_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject malloc_block: __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObjectCopy the code
There are three classes of block, as printed above: __NSGlobalBlock__, __NSStackBlock__, and __NSMallocBlock__. One might wonder if printing the second block will produce a different output under ARC and MRC. This is because in ARC mode, if a block of type __NSStackBlock__ is referenced by a strong pointer, the system will automatically copy the block to __NSMallocBlock__, which will affect the result of the run.
Global type (NSGlobalBlock)
If a block does not access ordinary local variables (that is, it does not access any external variables or access static local variables or global variables), then the block is __NSGlobalBlock__. Blocks of type __NSGlobalBlock__ have data areas in memory (also known as global or static areas, where global and static variables are stored). A block of type __NSGlobalBlock__ calling copy does nothing.
Stack type (NSStackBlock)
If a block has access to a normal local variable, it is an __NSStackBlock__, which is stored in memory in the stack area. The stack area is not controlled by the developer, but is managed by the system, so it is important to call __NSStackBlock__. Make sure it hasn’t been released yet. If you copy a block of type __NSStackBlock__, it copies the block from the stack to the heap.
Heap type (NSMallocBlock)
An __NSStackBlock__ block calls copy, which copies the block from the stack to the heap. The block on the heap is __NSMallocBlock__, so __NSMallocBlock__ blocks are stored in the heap. If you copy a block of type __NSMallocBlock__, the block’s reference count is +1.
In an ARC environment, the compiler automatically copies blocks on the stack to the heap, depending on the situation. Stack blocks can be copied onto the heap in four ways:
- Block as a function return value
- When assigning a block to a strong pointer
- When a block is a function argument
- When block is used as an argument to GCD
Modifiers in blocks
__weak modifier
In solving the problem of block circular reference, we often use the __weak keyword to modify the object that will cause circular reference, to solve the problem of object circular reference.
- Circular reference problem
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
void (^block)(void) = ^{
NSLog(@"age--- %ld",person.age);
};
block();
}
return 0;
}
Copy the code
Switch to C++ to view
// The underlying structure
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
};
Copy the code
As you can see, in contrast to the basic data type, the Person object is captured by a block and has a modifier __strong in its structure.
If you use _weak to decorate a variable after a reference
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
__weak Person *weakPerson = person;
void (^block)(void) = ^{
NSLog(@"age--- %ld",weakPerson.age);
};
block();
}
return 0;
}
Copy the code
/ / the bottom block
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
};
Copy the code
It can be seen that the key word of weakPerson in the block becomes __weak.
The key word used to modify the captured object type variable in a block is __strong, __weak, and __unsafe_unretained. So what does this result keyword do?
The copy function is called when a block is copied to the heap. The copy function calls the _Block_object_assign function internally. The _Block_object_assign function operates on these three keys.
If the key is __strong, then a retain operation is performed on the object inside the block, and the reference count is +1, which means that the block strongly references the object. For this reason, it is easy to create circular references when using blocks. If the key word is __weak or __unsafe_unretained, the block is a weak reference to the object and does not cause a circular reference. So we typically define a __weak or __unsafe_unretained pointer outside of a block to point to an object, and then use that weak pointer inside the block to solve the recurring reference problem.
__strong modifier
__strong does the opposite. It strongly references an object and makes the reference count +1.
Think about it: Blocks often use __weak to solve the problem of circular references caused by blocks capturing objects, so why use __strong to strongly reference objects?
-
Common scenarios for using __strong
One of the problems encountered during development is that the block does not strongly apply objects because it uses an __weak modifier outside the block, so the referenced objects are freed during block execution.
- (void)viewDidLoad { [super viewDidLoad]; Person *p = [[Person alloc]init]; p.age = 10; __weak Person *weakP = p; void(^block)(void) = ^(){ for (int i=0; i<3; NSLog(@" execute: %d", I); [NSThread sleepForTimeInterval:1]; } NSLog(@"%p age :%d",weakP, weakp.age); }; NSThread *thread = [[NSThread alloc]initWithBlock:block]; [thread start]; NSLog(@"========="); }Copy the code
Output:
2021-04-30 13:51:02.098873+0800 DEV[61283:8756387] ========= 2021-04-30 13:51:02.099007+0800 DEV[61283:8756387] Destroyed 2021-04-30 13:51:02.099189+0800 DEV[61283:8756467] 0 2021-04-30 13:51:03.100116+0800 DEV[61283:8756467] 1 2021-04-30 13:51:04.102146+0800 DEV[61283:8756467] Execute: 2 2021-04-30 13:51:05.107114+0800 DEV[61283:8756467] 0x0 Age :0Copy the code
You can see that the Person object is destroyed during the execution of the block. So how can you do this without creating a circular reference and without freeing the object referenced during block use? This is where the __strong modifier is used to solve the problem.
- How to use __strong
- (void)viewDidLoad { [super viewDidLoad]; Person *p = [[Person alloc]init]; p.age = 10; __weak Person *weakP = p; void(^block)(void) = ^(){ __strong Person* strongP = weakP; for (int i=0; i<3; NSLog(@" execute: %d", I); [NSThread sleepForTimeInterval:1]; } NSLog(@"%p age :%d",strongP,strongP.age); }; NSThread *thread = [[NSThread alloc]initWithBlock:block]; [thread start]; NSLog(@"========="); }Copy the code
Output:
2021-04-30 13:58:42.561851+0800 DEV[61444:8764380] ========= 2021-04-30 13:58:42.561951+0800 DEV[61444:8764483] Run: 0 2021-04-30 13:58:43.563091+0800 DEV[61444:8764483] Execution: 1 2021-04-30 13:58:44.569960+0800 DEV[61444:8764483] Execution: 2 2021-04-30 13:58:45.570770+0800 DEV[61444:8764483] 0x60000354C240 Age :10 2021-04-30 13:58:45.571068+0800 DEV (61444-8764483)Copy the code
Use _strong to strongly reference an object inside a block and then release the object after the block completes execution.
The function of the __block modifier
Before we get to __block, let’s look at the following code:
- (void)test{
int age = 10;
void (^block)(void) = ^{
age = 20;
};
}
Copy the code
What’s wrong with this code? The compiler reports an error and cannot change the age value in a block. Why is that?
Because the age is a local variable, its scope and life cycle is only in the test method, and also introduced the front, will block the bottom braces in the code in a function, also is the equivalent of now is to visit the test method in another function of the local variable, this is certainly not enough, so he complains. What do I do if I want to change the age value in a block? We can define age as a static local variable static int age = 10; . A static local variable has the same scope as the test method, but its life cycle is the same as that of the program, and the block captures the static local variable by capturing the address of the age, so the block changes the value of the age through the address of the age, so there is no problem. This is not recommended because static local variables are not released during program execution, so use them sparingly. What other ways are there to implement this requirement? This is the __block keyword.
- (void)test1{
__block int age = 10;
void (^block)(void) = ^{
age = 20;
};
block();
NSLog(@"%d",age);
}
Copy the code
When we use the __block keyword, what does the underlying layer do to allow us to access age inside the block? Let’s take a look at what the structure of the block looks like when the above code is converted to c++ code.
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
};
struct __Block_byref_age_0 {
void *__isa; / / isa pointer
__Block_byref_age_0 *__forwarding; // If the block is on the heap, the pointer points to itself. If the block is on the stack, the pointer points to the block after it is copied onto the heap
int __flags;
int __size; // Structure size
int age; // The age that was actually captured
};
Copy the code
Age with a __block modifier becomes __Block_byref_age_0 *age in the block structure. __Block_byref_age_0 is a structure with a member int age; This is the real captured external variable age, and actually the address of the external variable age also points to here, so when you change the age either outside of the block or inside the block, you actually change the age by going to this address. So age with __block is no longer a local variable inside a test1 method. Instead, it is wrapped into an object in which age is stored. Wrapped as an object because the first member of the __Block_byref_age_0 structure is the ISA pointer. __block decorates the memory management of variables: __block wraps either the underlying data type or the object data type as an object (I’ll call it __blockObj), with a pointer to __blockObj in the block structure. Since it’s an object, how does a block manage its memory internally? When a block is on the stack, there is no strong reference to __blockObj inside the block. When a block calls copy from the stack to the heap, it copies __blockObj to the heap and makes a strong reference to __blockObj. When a block is removed from the heap, its internal dispose function is called, which in turn calls the _Block_object_dispose function to release __blockObj.
The difference between delegate and block
-
Understand and distinguish between blocks and delegates at their source
The delegate is cheap to run and the block is expensive to run. Block off the stack requires copying the data used from stack memory to heap memory, of course, in the case of objects, counting, until used up or block set nil. The delegate simply holds a pointer to an object, which is called back directly with no extra cost. Just like C’s function pointer, it only does one more table lookup.
-
Distinguish a block from a delegate from a usage scenario
There are several related methods. This would be more troublesome if each method set a block. The delegate lets multiple methods be grouped together and set up once, allowing multiple callbacks. Delegate should be preferred when there are more than three methods. Block is used when one or two callbacks occur. The delegate is safer, for example: avoid circular references. When blocks are used, a circular reference can be formed without any attention, and the object cannot be freed. Such circular references are difficult to detect once they occur. The delegate method is isolated and does not reference the context, so it is more secure.
Read reference Block Underlying principle Block underlying implementation