__NSGlobalBlock__, __NSStackBlock__, __NSMallocBlock__, __NSMallocBlock__. Block capture of external variables and circular reference problems caused by block are analyzed.

This article will continue to analyze the underlying implementation principle of block, stack block is how to copy the heap, block capture the nature of external variables, block data structure and other content.

1. The principle of block

The old way, using clang to compile a.m file into a.cpp file, look at the nature of the block. `

1. No external variables are captured

Case code:

int main(int argc, char * argv[]) { @autoreleasepool { void (^block)(void) = ^{ NSLog(@"hello block"); }; block(); NSLog(@"%@", block); return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}Copy the code

The.cpp file is generated after clang. Look at the compiled main function:

  • A block is a structure defined as __main_block_IMPL_0, which inherits from __block_IMPl

    struct __block_impl { void *isa; // isa pointer int Flags; int Reserved; void *FuncPtr; // func };Copy the code
  • The __main_block_IMPL_0 constructor is provided in the block structure, which sets the related attributes in the block structure

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    Copy the code
  • The __main_block_IMPL_0 constructor takes the first argument to the __main_block_func_0 method implementation address, which encapsulates the task function of the block in the FuncPtr property when the block is declared

  • When you call block execution, you actually call block->FuncPtr and pass the block structure as a parameter to the method implementation

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    Copy the code
  • conclusion

    From the above analysis, we can know that a block is a structure, which can also be considered as a function or parameter. By functional programming, the block task is saved in the FuncPtr property of the structure. The function calls block->FuncPtr(), which holds all the data in the block as a hidden argument.

2. Catch external variables that are not decorated with __block

Modify the above example to capture external variables. See the following code:

int main(int argc, char * argv[]) { @autoreleasepool { int a = 10; void (^block)(void) = ^{ NSLog(@"hello block %d", a); }; block(); NSLog(@"%@", block); return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}Copy the code

We all know that blocks have external variable capture, so how is this capture implemented? See the compiled implementation:

From the code above:

  • When external variables are captured,blockThere will be one more member variable in the structurea, and the constructor takes an extra argumenta
  • If there is no__blockModifier, by way of value copy, to its member variablesaFor the assignment
  • In the implementationblockTask, obtains the corresponding member variables from the structure__cself->a, for processing

Capture variable, in the compile phase automatically generated the corresponding attribute variable, to store the value captured outside, variable value copy. You can’t change a constant because it’s a copy of a value, and you’ll have the same variable value internally and externally, which can cause code ambiguity! Compile however!

3. Capture external variables decorated with __block

To modify the above example, add __block to the external variable. See the following code:

int main(int argc, char * argv[]) { @autoreleasepool { __block int a = 10; void (^block)(void) = ^{ NSLog(@"hello block %d", a); }; block(); NSLog(@"%@", block); return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}Copy the code

What is the difference between an external variable modified by __block?

  • When an external variable is decorated with a __block, it is encapsulated in a structure __Block_byref_a_0

  • Inside the block structure, there is an additional attribute, a, of type __Block_byref_a_0

  • The address of A is assigned to the __forwarding property of the __Block_byref_a_0 structure

  • Conclusion:

    When a variable is modified with the __block modifier, block creates a structure for it at compile time. The structure preserves the address of the variable, a copy of the address. When used, the variable is actually accessed as a pointer, so changing it in a function or block does not affect each other. It can be verified by the following figure:

4. Capture variable summaries

The above analysis of the block to capture the logic of external variables, let’s use two cases to verify.

  • External variables that are not decorated with __block

    In this case, the reference count of objC is tracked and it changes from 1 to 3 after being captured inside the block. In fact, the previous article # Block and the circular reference problem had similar cases and analyzed the variation of reference counts.

    In this case, after objc is created in the outer layer, the reference count is 1; Because objc is not decorated with __block, it is handled by copying values; At the same time, because the block captures external variables, it is copied from the stack to the heap at run time, so that objC’s reference count is increased by one again. The final objC reference count is 3.

  • External variables decorated with __block

    Objc is now decorated with __block, and the object’s reference count is always 1. Because the external variables modified by __block are captured in the block structure by a pointer copy, the same object is operated on internally and externally. So the reference count is always 1.

2. Block source exploration

Set a breakpoint at the block definition:

Run the program and view the assembly, as shown below:

Through the assembly code, you can see that the objc_retainBlock method is called at the bottom. Set objc_retainBlock’s symbolic breakpoint and continue with the program:

It turns out that the objc_retainBlock method comes from libobjC.a.dylib, the most familiar objC library. A corresponding method implementation is also found in the libobjc.a.dylib library, which calls _Block_copy, but the implementation of _Block_copy is not in the libobjc.a.dylib library. See below:

Continue tracing the assembly, set _Block_copy to breakpoint, and run the program as shown below:

You can find _Block_copy in the libsystem_blocks. Dylib library.

At the same time, we can also see the source of block source in the CPP file obtained by clang, block_private.h. See below:

The __block_impl structure defined by block in the CPP file is the same as the Block_layout structure in the source code, as shown in the following figure:

3. Block memory changes

You learned about the three types of blocks in the previous article # Block and the circular reference problem. In particular, blocks that capture external variables are stack blocks at compile time but are copied to the heap area at run time and become heap blocks. So we’re going to look at when and how this memory change in a block happens.

To introduce the following example, the block captures an external variable and sets a breakpoint to trace it, as shown below:

By the way of tracking assembly, track its memory change process. When the program runs to objc_retainBlock, the data state change of the block is analyzed by reading the register. See below:

When the objc_retainBlock method is called, it is still a stack block. Continue tracing the assembly to _Block_copy. It is clear that the block has completed the memory change using this method. How to verify this? The assembly process is very long. Set a breakpoint in retq, that is, set a breakpoint in return, and see the final processing result, as shown below:

Also by reading register X0, the block is now __NSMallocBlock__ and the address has changed, copied from the stack to the heap.

At this point, it can be concluded that blocks that capture external variables are stack blocks at compile time and are copied to the heap area at run time using the _Block_copy method, becoming heap blocks.

4.Block_layout structure analysis

The Block_layout source code is defined as follows:

    struct Block_layout {
        void * __ptrauth_objc_isa_pointer isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        BlockInvokeFunction invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };
    
    #define BLOCK_DESCRIPTOR_1 1
    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };

    #define BLOCK_DESCRIPTOR_2 1
    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        BlockCopyFunction copy;
        BlockDisposeFunction dispose;
    };

    #define BLOCK_DESCRIPTOR_3 1
    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
Copy the code

Block_layout contains some properties:

  • isa isaTo determine theblocktype
  • flagsIdentification code
  • reservedKeep field
  • invokeThe delta function, which is thetaFuncPtr
  • descriptorRelated Additional information

Where the value of flag describes the block object. Let’s take a look at the definition of flag:

The Block_descriptor_1 attribute of the block must exist. Reserved indicates the reserved field and size indicates the block size. But Block_descriptor_3 is optional. The flag field is used to determine whether the block contains the Block_descriptor_3 attribute. The get method of the Block_descriptor can find out, get the corresponding value by translation, and when it gets the Block_descriptor_3 it can tell whether the Block_descriptor_2 exists. If not, You don’t need to add Block_descriptor_2 to the address space. See below:

We can use LLDB to verify:

Get the block’s memory space, translation 3*8 bytes to the Block_descriptor_1 address. We can view the memory space after Block_descriptor_1 to analyze the information about Block_descriptor_2 and Block_descriptor_3.

For example, we can print block signature information, as shown below:

The signature is in the signature attribute of Block_descriptor_3.

5._Block_copy Process analysis

_Block_copy implementation source code:

Void *_Block_copy(const void *arg) {struct Block_layout *aBlock; if (! arg) return NULL; // The following would be better done as a switch statement aBlock = (struct Block_layout *)arg; If (aBlock->flags & BLOCK_NEEDS_FREE) {// latches on high latching_incr_int(&aBlock->flags); return aBlock; } block else if (aBlock->flags & BLOCK_IS_GLOBAL) {return aBlock; } // The editor can not apply heap, only stack, Else {// stack // Its a stack block. Make a copy *)malloc(aBlock->descriptor->size); if (! result) return NULL; // Copy block memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first #if __has_feature(ptrauth_calls) // Resign the invoke pointer as it uses address authentication. result->invoke = aBlock->invoke; / / invoke assignment # endif / / reset refcount result - > flags & = ~ (BLOCK_REFCOUNT_MASK | BLOCK_DEALLOCATING); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1 _Block_call_copy_helper(result, aBlock); // Set isa last so memory analysis tools see a fully-initialized object. result->isa = _NSConcreteMallocBlock; // isa change return result; }}Copy the code
  • First of all toblockType strong, converted toBlock_layouttype
  • Determine whether to release the device. If yes, do not process the device
  • Determine whetherGLOBALIf yes, you don’t need tocopyoperation
  • If it is notGLOBAL, that is either stack or heap, andIt is impossible for the compiler to compile a heap block, so the source codeelseThe branch is rightStack block processing
  • callmallocTo initialize a memory space, passmemmoveCopy relevant data and performinvokeandflagThe setting of the
  • Will eventuallyblocktheisaSet to_NSConcreteMallocBlock

6. Block captures external variables

How does a block capture an external variable, what does a triple copy of a block look like? Back to the CPP file! View the definition of the __main_block_desc_0 structure. See below:

This structure is the Block_descriptor information in the corresponding source code. Where reserved and size correspond to the two properties Block_descriptor_1. In addition, void (*copy) and void (*dispose) correspond to the two methods Block_descriptor_2; In an implementation of the copy method, _Block_object_assign is called, which is the process of capturing and releasing external variables.

A global search for _Block_object_assign in the source code yields the following comment information:

Helper functions are provided by the compiler for Block_copy and Block_release, called copy and dispose helper functions. The copy helper makes calls to C++ const constructors for C++ stack-based objects and supports the _Block_object_assign function at runtime for them. Dispose helper calls _Block_object_dispose on the C++ destructor.

Flags for _Block_object_assign and _Block_object_dispose are set to:

  • BLOCK_FIELD_IS_OBJECT (3)To capture theObjective-C ObjectIn the case
  • BLOCK_FIELD_IS_BLOCK (7)And capture anotherblockIn the case
  • BLOCK_FIELD_IS_BYREF (8)To capture the__blockCase of variables

The enumeration definition is shown below:

The ones we use most are BLOCK_FIELD_IS_OBJECT and BLOCK_FIELD_IS_BYREF.

  • _Block_object_assign source code analysis

    void _Block_object_assign(void *destArg, const void *object, const int flags) { const void **dest = (const void **)destArg; switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) { case BLOCK_FIELD_IS_OBJECT: _Block_retain_object(object); *dest = object; break; case BLOCK_FIELD_IS_BLOCK: *dest = _Block_copy(object); break; case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK: case BLOCK_FIELD_IS_BYREF: *dest = _Block_byref_copy(object); break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK: *dest = object; break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK: *dest = object; break; default: break; }}Copy the code
    • If the holding variable is thetaBLOCK_FIELD_IS_OBJECTType, that is, none__blockModifier, pointer to the object, will hold the object, reference count increment1
      *dest = object;
      Copy the code
    • If it isBLOCK_FIELD_IS_BLOCKType, and capture oneblock, do_Block_copyoperation
      *dest = _Block_copy(object);
      Copy the code
    • If it is BLOCK_FIELD_IS_BYREF, it is__blockModifier, is called_Block_byref_copy
      *dest = _Block_byref_copy(object);
      Copy the code
  • _Block_byref_copy implementation source code

    static struct Block_byref *_Block_byref_copy(const void *arg) { struct Block_byref *src = (struct Block_byref *)arg; If ((SRC ->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {// SRC points to stack struct Block_byref *copy = (struct Block_byref *)malloc(src->size); copy->isa = NULL; // byref value 4 is logical refcount of 2: one for caller, one for stack copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4; copy->forwarding = copy; // patch heap copy to point to itself src->forwarding = copy; // patch stack to point to heap copy copy->size = src->size; if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // Trust copy helper to copy everything of interest // If more than one  field shows up in a byref block this is wrong XXX struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1); struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1); copy2->byref_keep = src2->byref_keep; copy2->byref_destroy = src2->byref_destroy; if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) { struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1); struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1); copy3->layout = src3->layout; } // Capture external variables - memory processing - life cycle save (*src2->byref_keep)(copy, SRC); } else { // Bitwise copy. // This copy includes Block_byref_3, if any. memmove(copy+1, src+1, src->size - sizeof(*src)); } } // already copied to heap else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } return src->forwarding; }Copy the code
    • Encapsulate the external object as a structure Block_byref * SRC

    • If it is BLOCK_FIELD_IS_BYREF, malloc is called and a Block_byref *copy is generated

    • Set forwarding to ensure that both inside and outside a block point to the same object

          copy->forwarding = copy;
          src->forwarding = copy;
      Copy the code
    • The keep and destroy functions are handled in Block_byref and the byref_keep function is called

      The design roadmap for Block_byref is similar to that for Block_layout descriptor. Use the byref->flag identifier to determine whether Block_byref_2 exists. The definition of Block_byref is as follows:

      If an external variable is modified with __block, two methods are generated by default in the Block_byref structure of the CPP file, that is, the keep method corresponding to Block_byref_2 and the destory method, as shown below:

      Search the CPP file for the implementations of these two functions, as shown below:

      This process calls the _Block_object_assign function again to perform the BLOCK_FIELD_IS_OBJECT process on the objects in the Block_byref structure.

The triple copy of the block is now known:

  1. blockCopy of the stack areablock, to the heap
  2. __blockDecorates the object corresponding toBlock_byrefA copy of the structure
  3. rightBlock_byrefDecorates the object called_Block_object_assignThe function is decorated

At this point, the completion of the block to capture the nature of external variables, block underlying implementation principle analysis, block data structure analysis and other content.