The interview questions

  1. How does block work? What is the essence?
  2. What does __block do? What is the use of attention?
  3. Why is the property modifier copy for block? What are the implications of using blocks?
  4. Block is modifying NSMutableArray. Do I need to add __block?

Start with a basic understanding of blocks

A block is essentially an OC object with an ISA pointer inside it. A block is an OC object that encapsulates a function call and its environment.

Explore the nature of blocks

Let’s start with a simple block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(int ,int) = ^(int a, int b){
            NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age); }; Block (3, 5); }return 0;
}
Copy the code

Use the command line to convert the code to c++ to see its internal structure compared to the OC code

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

In the figure above, the c++ block declaration and definition are shown as corresponding to oc code. Separate the declarations and calls to a block in c++ to see its internal implementation.

Defining block variables

/ / code block variables void block (*) (int, int) = ((void (*) (int, int)) & __main_block_impl_0 (__main_block_func_0 (void *), &__main_block_desc_0_DATA, age));Copy the code

The block definition calls __main_block_IMPL_0 and assigns the address of __main_block_IMPL_0 to the block. So let’s look at the internal structure of the __main_block_IMPL_0 function.

__main_block_imp_0 structure

The __main_block_imp_0 structure has a constructor of the same name, __main_block_imp_0, which assigns some variables to return a structure.

This means that the block variable is finally assigned the address of a __main_block_imp_0 structure

Inside the __main_block_IMPL_0 structure, you can find four arguments passed in the __main_block_IMPL_0 constructor. (void *)__main_block_func_0, &__main_block_desc_0_DATA, age, flags. Flage has a default value, which means that the flage parameter can be omitted when called. The final age(_age) indicates that the passed _age argument is automatically assigned to the age member, equivalent to age = _age.

Let’s focus on what the first three parameters represent.

(void *)__main_block_func_0

The __main_block_func_0 function first takes the value of age in the block, followed by two familiar NSlogs that are exactly the code we wrote in the block. So the __main_block_func_0 function actually stores the code we wrote in the block. The __main_block_impl_0 function is passed (void *)__main_block_func_0, which encapsulates the code we wrote in the block as __main_block_func_0, The address of the __main_block_func_0 function is passed into the __main_block_impl_0 constructor.

&__main_block_desc_0_DATA

__main_block_desc_0 stores two parameters, reserved and Block_size. Reserved is assigned to 0, and Block_size stores the size of __main_block_IMPL_0. Finally, pass the address of the __main_block_desc_0 structure into __main_block_func_0 and assign it to Desc.

age

Age is the local variable that we defined. Since age is used in the block, age is passed as a parameter when the block is declared. If age is not used in the block, only (void *)__main_block_func_0 is passed. __main_block_desc_0_DATA has two parameters.

Here’s a look at the source code for why modifying the local age variable after defining a block doesn’t work when the block is called.

int age = 10;
void(^block)(int ,int) = ^(int a, int b){
     NSLog(@"this is block,a = %d,b = %d",a,b);
     NSLog(@"this is block,age = %d",age); }; age = 20; Block (3, 5); //log: this is block,a = 3,b = 5
     //      this is block,age = 10
Copy the code

Changes to local variables after a block definition are not captured by the block because the block has passed in the value of age and stored it in the __main_block_imp_0 structure and takes age out of the block when called.

At this point, look back at the __main_block_IMPL_0 structure

First let’s look at __block_impl. The first variable is the __block_impl structure. Inside the __block_impl structure

We can see that there is an ISA pointer inside the __block_impl structure. So you can prove that a block is essentially an OC object. Instead, the constructor stores the values passed in to the function in an instance of the __main_block_IMPL_0 structure, and eventually assigns the address of the structure to the block.

The __main_block_IMPL_0 constructor has three parameters:

1. The __block_impl isa pointer holds the &_NSConcretestackblock address. Block is of type _NSConcreteStackBlock.

2. The code in the block is encapsulated as the __main_block_func_0 function, and FuncPtr stores the address of the __main_block_func_0 function.

3. Desc points to the __main_block_desc_0 structure object, which stores the memory occupied by the __main_block_IMPL_0 structure.

Call block to execute internal code

/ / implementation code within the block (void (*) (__block_impl *, int, int)) ((__block_impl *) block) - > FuncPtr) (block (__block_impl *), 3, 5);Copy the code

Block (__main_block_IMPL_0); FunPtr (__main_block_impl_0); FunPtr is not directly found in __main_block_IMPL_0. FunPtr is stored in __block_impl. Why can block call FunPtr directly in __block_impl?

A review of the above source code shows that (__block_impl *)block forces a block to be of type __block_impl, since __block_impl is the first member of the __main_block_IMPL_0 structure, This means that the __block_impl memory address begins with the __main_block_IMPL_0 memory address by placing the members of the __block_impl structure directly in __main_block_IMPL_0. So it works. And find FunPtr members.

FunPtr contains the address of a function wrapped in a code block, so calling this function executes the code in the code block. And if you look back at the __main_block_func_0 function, you can see that the first argument is a pointer of type __main_block_IMPL_0. That is, passing a block to the __main_block_func_0 function makes it easier to retrieve the value captured by the block.

How to verify that the essence of a block is indeed a __main_block_IMPL_0 structure type.

To prove the above, we use the same method as before, we define the structure of the block inside the structure of the above analysis, and force the structure of the block inside the structure to the custom structure. The successful transformation shows that the underlying structure is exactly the same as we analyzed before.

struct __main_block_desc_0 { size_t reserved; size_t Block_size; }; struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; Struct __main_block_impl_0 {struct __block_impl impl; struct __main_block_desc_0* Desc; int age; }; int main(int argc, const char * argv[]) { @autoreleasepool { int age = 10; void(^block)(int ,int) = ^(int a, int b){ NSLog(@"this is block,a = %d,b = %d",a,b);
            NSLog(@"this is block,age = %d",age); }; // Force the underlying structure into our own structure, Struct __main_block_IMPL_0 *blockStruct = (__bridge struct __main_block_IMPL_0 *)block; Block (3, 5); }return 0;
}
Copy the code

The break points show that our custom structure can be successfully assigned, as well as the values inside.

Next the breakpoint goes to the block and looks at the address of the function call in the stack information. Debuf workflow -> always show Disassembly

You can see from the figure above that the address is exactly the same as the code block address in FuncPtr.

conclusion

Now that you have a basic understanding of the underlying structure of a block, the code above can be shown in a diagram showing the relationships between the various structures.

The underlying data structure of a block can also be shown in a diagram

Block variable capture

To ensure that external variables can be accessed from within a block, a variable capture mechanism is used.

A local variable

Auto variable

We’ve already seen how blocks capture the age variable. Auto automatic variable, destroyed when out of scope, usually preceded by the auto keyword. Automatic variables are captured inside the block, which means that a new parameter is added inside the block to store the value of the variable. Auto only exists in local variables and is accessed by value passing. From the above explanation of the age parameter, we can also confirm that it is indeed value passing.

Static variables

Static modified variables are passed as Pointers and are also caught by the block.

Next, add the aoTU modifier and the static modifier, and review the source code to see the difference.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        auto int a = 10;
        static int b = 11;
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return0; } / /log[57465:18555229] Hello, a = 10, b = 2 // The value of a in the block is not changed and the value of B is changed with external changes.Copy the code

Regenerate the c++ code to see the difference between the two parameters in the internal structure.

As can be seen from the above source code, both a and B variables are captured inside the block. But A passes in a value, and B passes in an address.

The reason for this difference is that automatic variables can be destroyed, so it is possible that the automatic variable has already been destroyed at the time the block is executed. If you try to access the destroyed address, you will get bad memory access. Therefore, automatic variables must be passed by value, not by pointer. Static variables are not destroyed, so you can pass the address. Because the value of the address is passed, the address in the block will not change if the value is changed before the block is called. So the value will change.

The global variable

Let’s also code to see if the block captures global variables

int a = 10;
static int b = 11;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"hello, a = %d, b = %d", a,b);
        };
        a = 1;
        b = 2;
        block();
    }
    return0; } / /log hello, a = 1, b = 2
Copy the code

Also generate c++ code to see how global variables are called

As you can see from the above code, __main_block_imp_0 does not add any variables, so the block does not need to capture global variables, which can be accessed from anywhere.

Local variables need to be captured because they are accessed across functions. Global variables can be accessed anywhere, so they are not captured.

Finally, make a conclusion with a picture

Summary: Local variables are captured by block, automatic variables are captured by value, static variables are captured by address. Global variables are not captured by blocks

Question: Does the block catch variables in the following code?

#import "Person.h"
@implementation Person
- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"% @",self);
    };
    block();
}
- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}
+ (void) test2
{
    NSLog(@"Class method test2");
}
@end
Copy the code

Also convert to c++ code to see its internal structure

As you can see in the figure above, self is also caught by the block, and then we find the test method, which by default passes two arguments, self and _cmd. The class method test2 also passes the class object self and the method selector _cmd by default.

By default, both object and class methods pass self inside the method as an argument. Since self is passed in as an argument, it must be a local variable. It says that local variables must be caught by blocks.

Next, let’s see what happens if you use a member variable in a block or call an instance’s property.

- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"% @",self.name);
        NSLog(@"% @",_name);
    };
    block();
}
Copy the code

As you can see in the figure above, even though the block uses the attributes of the instance object, the block still captures the instance object, and obtains the used attributes through the instance object in different ways.

The type of the block

The isa pointer in this block points to the address of the _NSConcreteStackBlock class. Is the block of type _NSConcreteStackBlock?

We look for specific types in code using class methods or ISA Pointers.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
        void (^block)(void) = ^{
            NSLog(@"Hello");
        };
        
        NSLog(@"% @", [block class]);
        NSLog(@"% @", [[block class] superclass]);
        NSLog(@"% @", [[[block class] superclass] superclass]);
        NSLog(@"% @", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}
Copy the code

Print the content

As you can see from the print above, blocks eventually inherit from NSBlock, which in turn inherits from NSObjcet. So the ISA pointer in the block actually comes from NSObject. This further confirms that blocks are OC objects in nature.

The three types of blocks

Blocks have three types

__NSGlobalBlock__ (_NSConcreteStackBlock) __NSStackBlock__ (_NSConcreteStackBlock) __NSMallocBlock__ ( _NSConcreteMallocBlock)Copy the code

Look at the code and see under what circumstances blocks are of different types

int main(int argc, const char * argv[]) { @autoreleasepool { // 1. Void (^block1)(void) = ^{NSLog(@)"Hello"); }; // 2. Block a = 10; void (^block2)(void) = ^{ NSLog(@"Hello - %d",a); }; // 3. The class NSLog(@) of the directly called block"% @ % @ % @", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}
Copy the code

There are really three types of blocks that can be found by printing content

The isa pointer to the three blocks in the c++ source code all points to the _NSConcreteStackBlock type address.

We can guess that there may have been a type shift during the Runtime runtime. The final type is of course the Runtime runtime type that we print.

The storage of blocks in memory

Take a look at the storage area of the different blocks in the figure below

As you can see in the figure above, blocks are stored in different regions depending on their type. __NSGlobalBlock__ is not recycled until the end of the program, but blocks of type __NSGlobalBlock__ are rarely used because they don’t make sense.

Blocks of type __NSStackBlock__ are stored on the stack. We know that memory in the stack is automatically allocated and freed as soon as the scope completes execution, and it seems unnecessary to define blocks and call blocks in the same scope.

__NSMallocBlock__ is the one most commonly used in everyday coding. Storing in the heap requires our own memory management.

How does a block define its type

How does a block define its type, and on what basis do you define different types of blocks and assign them to different Spaces? Let’s start with the following picture

We then use code to verify the above problem, first turning off ARC and returning to the MRC environment, because ARC helps us do a lot of things that might affect our observations.

// MRC environment!! Int main(int argc, const char * argv[]) {@autoreleasepool {// Global: no access to auto: __NSGlobalBlock__ void (^block1)(void) = ^{ NSLog(@"block1---------"); }; // Stack: access the auto variable: __NSStackBlock__ int a = 10; void (^block2)(void) = ^{ NSLog(@"block2---------%d", a);
        };
        NSLog(@"% @ % @", [block1 class], [block2 class]); // __NSStackBlock__ calls copy: __NSMallocBlock__ NSLog(@)"% @", [[block2 copy] class]);
    }
    return 0;
}
Copy the code

Viewing print Content

The printed content can be seen as shown in the picture above. Blocks that do not access the auto variable are of type __NSGlobalBlock__ and are stored in the data segment. Blocks that access the auto variable are of type __NSStackBlock__ and are stored on the stack. Block calls to copy of type __NSStackBlock__ become type __NSMallocBlock__ and are copied to the heap.

The __NSGlobalBlock__ type mentioned above is rarely used, because if you don’t need to access external variables, you can implement it directly through functions, without using blocks.

But __NSStackBlock__ accesses the AOTU variable and is stored on the stack. As mentioned above, the memory in the stack will be destroyed after the scope expires, so it is possible that the block memory will be destroyed before calling it. This will cause problems.

void (^block)(void);
void test()
{
    // __NSStackBlock__
    int a = 10;
    block = ^{
        NSLog(@"block---------%d", a);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test(a); block(); }return 0;
}
Copy the code

View the print

You can see that the value of A has changed to an uncontrollable number. Why does this happen? Since the blocks created in the above code are of type __NSStackBlock__, the blocks are stored on the stack, and when the test function completes, the memory occupied by the blocks in the stack has been reclaimed by the system, thus causing the possibility of cluttered data. Look at the c++ code for a clearer understanding.

To avoid this, you can use copy to convert blocks of type __NSStackBlock__ to blocks of type __NSMallocBlock__ and store the blocks in the heap. Here is the modified code.

void (^block)(void);
void test() {// __NSStackBlock__ calls copy to __NSMallocBlock__ int age = 10; block = [^{ NSLog(@"block---------%d", age);
    } copy];
    [block release];
}
Copy the code

The correct data will be found when printing

Does another type of block call copy change the block type? The table below shows it clearly.

Therefore, in the normal development process of MRC environment, we often need to use copy to save blocks, copy blocks on the stack to the heap, even if the block on the stack is destroyed, the block on the heap will not be destroyed, we need to call the release operation to destroy. In an ARC environment, the system automatically calls copy so that the block will not be destroyed.

What did ARC do for us

In an ARC environment, the compiler automatically copies blocks on the stack to the heap as needed.

When does ARC automatically copy a block? The following code is executed in the RAC environment.

1. When block is returned by a function

typedef void (^Block)(void);
Block myblock() { int a = 10; As mentioned earlier, the auto variable is called in the block, and the block type should be __NSStackBlock__ block block = ^{NSLog(@)"---------%d", a);
    };
    returnblock; } int main(int argc, const char * argv[]) { @autoreleasepool { Block block = myblock(); block(); // Prints the block type __NSMallocBlock__ NSLog(@"% @",[block class]);
    }
    return 0;
}
Copy the code

Take a look at the print

As mentioned earlier, if the auto variable is accessed in a block, the block is of type__NSStackBlock__, blCOK is found to be__NSMallocBlock__Type, and can print the value of A normally, indicating that block memory has not been destroyed.

As mentioned above, block copy converts to__NSMallocBlock__RAC will automatically help us copy the block as a function return value to save the block and release it where appropriate.

2. Assign block to the __strong pointer

RAC also automatically copies a block if it is referenced by a strong pointer.

Int main(int argc, const char * argv[]) {@autoreleasepool {// There is no access to the auto variable block block = ^{NSLog(@"block---------");
        };
        NSLog(@"% @",[block class]); int a = 10; // The block accesses the auto variable, but does not assign to the __strong pointer NSLog(@)"% @",[^{
            NSLog(@"block1---------%d", a); } class]); // Block assigned to the __strong pointer block block2 = ^{NSLog(@"block2---------%d", a);
        };
        NSLog(@"% @",[block1 class]);
    }
    return 0;
}
Copy the code

Looking at the print, you can see that RAC automatically does a copy operation when a block is assigned to a __strong pointer.

3. Block is used as a Cocoa API method name containing the usingBlock method parameter

For example, iterate over the block method of a number array, taking block as an argument.

NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
}];
Copy the code

4. Block as a method parameter of the GCD API

For example: GDC one-time functions or delayed functions, the system will not release the block until the block operation is completed.

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});        
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});
Copy the code

Block declaration

Through the above analysis of different types of blocks in MRC and ARC environments, the proposed writing method of block attributes in different environments is summarized.

Recommended writing method of block attribute under MRC

@property (copy, nonatomic) void (^block)(void);

Recommended way to write block properties under ARC

@property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void);

Underlying principles article column

Underlying principles article column


Welcome to point out any mistakes in the article. I am XX_CC, a long grown but not enough of a guy.