This is the 27th day of my participation in the August More Text Challenge

Block source code

1. Block underlying analysis

To explore what the underlying structure of a block is, define a block and xcrun it.

After xcRun, open the generated CPP file and see the generated main function.

After strong-casting the function, you get the following code. Here we see that void(*block)(void) is equal to the return value of the __main_block_IMPL_0 function, while the block call is equal to block->FuncPtr(block).

A search for __main_block_IMPL_0 indicates that block is equal to the constructor of the structure. What’s the connection between the main function that defines an int a and the structure that has an int A?

So let’s try to get rid of the A in the block, and then xcrun again. So here we see that the A in the structure disappears. And the arguments in the constructor and a(_a) are missing. A (_a) is a c++ syntax that assigns parameters passed in to the member variable a. That is: the block captures A at the bottom and forms corresponding member variables.

Let’s change int to NSObject and see that it does form the corresponding member variable. (NSObject is used here, so it goes to the viewController file and xcRun again.)

Also notice that the block structure isa points to _NSConcreteStackBlock, which captures external variables. Why stackBlock? Does _NSConcreteStackBlock at compile time change to mallocBlock at run time? There is also the parameter fp passed in here, which is assigned to FuncPtr of the IMPl. Fp is __main_block_func_0, then imp. FuncPtr = __main_block_func_0. Search __main_block_func_0 and find function implementations inside. So funcPtr is a function implementation of a block. The previous block->FuncPtr(block); This is a call to __main_block_func_0. This shows that block saves FP functionally.

Int a = __cself->a, __cself is the argument passed in the block itself, so __cself->a is the member variable a of the block structure. Int a is copied here.

2. __ Block

When declaring a, add the __block modifier.

Re-xcrun to see that int a in the structure and function has changed to __Block_byref_a_0 *a.

In main, the structure of a also becomes __Block_byref_a_0 and is initialized. And what’s stored here is ampersand a which is the address of A. Here we omit the int a = 18 step, and ampersand a is the address of this a.

Next, find the __Block_byref_a_0 structure, where *__forwarding is the address of A.

__Block_byref_a_0 *a is equal to __cself->a which is the address of A. At this point, the a inside __main_block_func_0 is the same as the A inside main because they have the same address, pointing to the same memory space. If you change the value of A inside the block, the value of a outside the block will change as well.

Therefore, __block generates the __Block_byref_a_0 structure and passes the pointer address to the block, thus modifying the same memory space.

3. Block underlying copy processing

Hit the breakpoint and run the following code.

Notice that there is an objc_retainBlock call, and then go inside the objc_retainBlock.

Objc_retainBlock can also be accessed by setting a symbolic breakpoint.

Then go to libobJC source code and search for objc_retainBlock and find that _Block_copy is called. _Block_copy was not found in libobjc, so go to the symbol breakpoint.

Found in libsystem_blocks. Dylib, this framework is not open source. This can be analyzed using disassembly or by replacing engineering libclosure79.

Go to libclosure7 and search for _Block_copy. Here you see that block really starts with the underlying structure Block_layout.

The member variables inside Block_layout are:

  1. Isa: Points to a class that indicates the block type

  2. Flags: identifies additional information about blocks in bits, similar to the bit fields in isa. Flags are of the following types, focusing on BLOCK_HAS_COPY_DISPOSE and BLOCK_HAS_SIGNATURE. BLOCK_HAS_COPY_DISPOSE Determines whether Block_descriptor_2 is present. BLOCK_HAS_SIGNATURE Determines whether Block_descriptor_3 is present

  3. Reserved: Reserved information. It can be understood that the reserved position is used to store information about internal variables of a block

  4. Invoke: Is a function pointer to the block’s executing code

  5. Descriptor: Additional information about a block, such as whether it is being destructed, whether it has a destructor, whether it has a keep function, etc. There are three types: Block_descriptor_1 must exist, Block_descriptor_2 and Block_descriptor_3 are both optional parameters.

So let’s see what’s in flags. See the following identifiers. This section focuses on BLOCK_HAS_COPY_DISPOSE and BLOCK_HAS_SIGNATURE. BLOCK_HAS_COPY_DISPOSE Determines whether Block_descriptor_2 is present. BLOCK_HAS_SIGNATURE Determines whether Block_descriptor_3 is present.

Go back to the block Copy method and read the register and see that the message receiver is of type NSGlobalBlock.

Go back to the viewController and let the block catch the external variable and run it again.

Re-read the register and output NSStackBlock with more copy and dispose. The block should be MallocBlock, why is it NSStackBlock? I’m going to change NSStackBlock to MallocBlock in copy.

Copy () {return (NSMallocBlock); return (NSMallocBlock);

Now look at how the source code works. First, the status of the block is determined. If it is BLOCK_NEEDS_FREE or BLOCK_IS_GLOBAL, aBlock is returned.

If neither, the block is processed. This is compile-time, so it’s a stackBlock, a stackBlock. Because it puts too much pressure on the compiler to open up memory at compile time, they are marked as stack blocks at compile time. At runtime, if the stack block also captures external variables, a copy will be made of this block and isa will be changed to _NSConcreteMallocBlock. The action here is to take the size of the block, create a new memory space based on that size, then call memmove to copy it, then copy the invoke and other attributes, and process the identifier before changing the block’s ISA. So the _Block_copy operation turns the stack block into the heap block.

When reading the register, it also prints signature: “v8@? 0”, that’s the signature of the block. Let me print it out. With the signature, you can do the corresponding hook processing. Block’s invoke is actually a message send, and if a message fails or a problem occurs, it enters the message forwarding process. In the slow forwarding process of message forwarding, signatures are required for related processing.

In the previous analysis, Block_layout had another descriptor, so what was inside that descriptor? It is found that reserved and size are loaded inside, so where are the copy and dispose printed out when reading the register?

You can see that copy and dispose are inside Block_descriptor_2. This involves continuous memory options. Different types of blocks have different structures, so the memory options are determined by identifiers. The identifiers here are BLOCK_DESCRIPTOR_2 and BLOCK_DESCRIPTOR_3.

So what determines an identifier? If no useful information is found in the setter, look for it in the corresponding getter. Search Block_descriptor_2, see the getter here. To get _Block_descriptor_2 or _Block_descriptor_3, you have to += sizeof(struct Block_descriptor_1), so Block_descriptor_1 must exist, _Block_descriptor_3 also requires aBlock->flags & BLOCK_HAS_COPY_DISPOSE to determine the presence of Block_descriptor_2 to determine whether += sizeof(struct) is required Block_descriptor_2).

The heap block that was printed before had copy, dispose and signature, so that means that the heap block has Block_descriptor_2 and _Block_descriptor_3 inside it.

Since it was a memory translation, then we can use a memory translation to get Block_descriptor_2 and _Block_descriptor_3. Where isa is 8 bytes and int32_t is 4 bytes, flags and reserved are 8 bytes. Invoke 8 bytes

X /8gx prints MallocBlock and turns 16 bits to invoke.

Descriptor is a Block_descriptor_1* type, which is 8 bytes, so 0x00000001006C40d0 is descriptor. X /8gx print descriptor, then the first 16 bytes are stored descriptor1, the next 16 bytes are stored descriptor2, and the next 16 bytes are stored descriptor3. And 0x00000001006C1308 and 0x00000001006C1344 are indeed the addresses of copy and Dispose printed before. Printing 0x00000001006C3477 also prints the signature.

4. _Block_object_assign

Here we see a __ViewController__viewDidLoad_block_desc_0 structure, which by default is equal to __ViewController__viewDidLoad_block_desc_0_DATA, That is to say the copy inside is equal to __ViewController__viewDidLoad_block_copy_0, dispose is equal to __ViewController__viewDidLoad_block_dispose_0. And copy and dispose are both descriptor2 items.

__ViewController__viewDidLoad_block_copy_0 calls _Block_object_assign. What does this method do?

Search the source code for _Block_object_assign and find this comment. Flags in _Block_object_assign and _Block_object_dispose will have different values depending on the variables.

  • Ordinary objectsThat is, there are no other reference types BLOCK_FIELD_IS_OBJECT (3)
  • Block typeAs a variableBLOCK_FIELD_IS_BLOCK (7)
  • after__blockModified variableBLOCK_FIELD_IS_BYREF (8)

Go back and set the variable to a normal object and re-xcrun, and it does become 3.

Search the source code for _Block_object_assign and find the implementation. Here we see that if it is a normal object type, then the _Block_retain_object operation is performed. Then copy the value of the object, pointing to the same memory space.

Look for _Block_retain_object and find that it is equal to _Block_retain_object_default, while _Block_retain_object_default is an empty implementation. So what does this mean? This is actually handed over to the system-level ARC for processing.

If the variable is of block type, the block is copied from the stack to the heap using the _Block_copy operation

If it is a __block decorated variable, the _Block_byref_copy function is called for memory copy and general processing

Next, search for the _Block_byref_copy function. Here we make a judgment, make sure the object is reference-counted, and let ARC take care of it. Here, memory space is opened up according to the original object, and corresponding copy processing is performed, and forwarding of both points to copy. Please dispose the Block and call srC2 byref_keep method to save the life cycle of the object.

SRC copy and dispose are members of the structure, and the variables are declared with these two parameters, so the copy of block_byref_2 is __Block_byref_id_object_copy_131. Destroy equals __Block_byref_id_object_dispose_131.

A search of __Block_byref_id_object_copy_131 shows that _Block_object_assign is called again. The + 40 will be shifted to objc1 in the __Block_byref_objc1_0 structure, which is the variable created in viewDidLoad, to perform the normal reference counting and variable assignment once for _Block_object_assign.

__block Three-layer copy:

  1. Block copy Copies blocks from the stack to the heap
  2. Block captures the variable block_byref and then copies block_byref
  3. Block_byref copies variables of an object