探寻Block的本质(1)—— 基本认识

1,265 阅读7分钟

Block传送门🦋🦋🦋

探寻Block的本质(2)—— 底层结构

探寻Block的本质(3)—— 基础类型的变量捕获

探寻Block的本质(4)—— Block的类型

探寻Block的本质(5)—— 对象类型的变量捕获

探寻Block的本质(6)—— __block的深入分析

block是什么

通俗的理解:block就是将一些代码封装起来,以便在将来某个时候被使用,如果你不去调用block,block内部封装的代码就不会执行。举一个简单的例子,下面在main函数中定义一个最简单的block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
        }; 
    return 0;
}

**********************   运行结果   ************************
Program ended with exit code: 0

运行程序运行可以看到block内的代码是没有运行的,因为没有调用。Block的使用也很简单,可以像函数一样被使用。加上()就代表调用,如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^{
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
        }(); 
    return 0;
}

**********************   运行结果   ************************
2019-05-28 17:18:56.992746+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992924+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992939+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992947+0800 Interview03-block[2640:180864] I am a block!
Program ended with exit code: 0

如果上面写的太简练不习惯的话,通常大家可能是这么写

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block)(void) = ^{
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
        };
        block();

**********************   运行结果   ************************
2019-05-28 17:18:56.992746+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992924+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992939+0800 Interview03-block[2640:180864] I am a block!
2019-05-28 17:18:56.992947+0800 Interview03-block[2640:180864] I am a block!
Program ended with exit code: 0

至于block的书写语法请自行搞定。

你可能在面试中被这么问过:Block的本质是什么?

显然,仅仅通过上面的知识,肯定不会让面试官满意的。这里介绍一张block的底层结构图 关于此图有如下二点解释:

  • block的本质也是一个OC对象,它内部也有个isa指针
  • block是封装了函数调用函数调用环境的OC对象

第一点很容易看出,block地层结构图中的第一个成员就是一个isa指针,所以我们可以将block当成一个对象来看待。那么第二点中的 函数调用函数调用环境是什么意思呢?我们一步一步来。 我们先将上面的代码扩展一下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^block)(int, int) = ^(int c, int b){
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"c = %d",c);
            NSLog(@"b = %d",b);
            NSLog(@"a的值为%d",a);
        };
        block(50,100);
    }
    return 0;
}

**********************   运行结果   ************************
2019-05-29 17:13:56.021422+0800 Interview03-block[9455:836127] I am a block!
2019-05-29 17:13:56.021630+0800 Interview03-block[9455:836127] I am a block!
2019-05-29 17:13:56.021639+0800 Interview03-block[9455:836127] c = 50
2019-05-29 17:13:56.021649+0800 Interview03-block[9455:836127] b = 100
2019-05-29 17:13:56.021700+0800 Interview03-block[9455:836127] a的值为10
Program ended with exit code: 0

上面的代码可以看出,block里面使用了它上面的 int a = 10 ,可以将这个先简单的理解成函数调用环境,顾名思义,就是block所用到的一些外部变量。 函数调用指的就是上面包含那5句打印代码的匿名函数,block被调用的时候,该函数就会被调用。 接下来,我们在命令行中,使用

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

将main.m文件转换成C++中间代码,来仔细研究一下block的底层构造。在main.cpp文件中,将代码直接拉到底部,可以发现我们需要的内容。核心代码整理后如下,下面的代码展示了运行状态下block内的实际内容

#import <Foundation/Foundation.h>

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;
    
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        
        int a = 10;
        
        void (^block)(int, int) = ^(int c, int b){
            NSLog(@"I am a block!");
            NSLog(@"I am a block!");
            NSLog(@"c = %d",c);
            NSLog(@"b = %d",b);
            NSLog(@"a的值为%d",a);
            
        };
        
        struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)block;
        
        block(50,100);
        

    }
    return 0;
}

我们将block的底层结构struct __main_block_impl_0直接般到main.m里面,同时,还需要将struct __block_impl以及struct __main_block_desc_0也搬过来,以保证编译正确,这样,我们便可以通过struct __main_block_impl_0来读取运行时下,block中的内容

struct __main_block_impl_0 *tmpBlock = (__bridge struct __main_block_impl_0 *)block;

我们可以在调试窗口看到如下信息运行时下block内部的信息 接下来我们在block内部的代码段加上断点 然后通过Debug-->DebugWorkflow-->Always Show Disassembly查看一下此时的汇编代码

Interview03-block`__main_block_invoke:
    0x100000e90 <+0>:   pushq  %rbp
    0x100000e91 <+1>:   movq   %rsp, %rbp
    0x100000e94 <+4>:   subq   $0x20, %rsp
    0x100000e98 <+8>:   leaq   0x1c1(%rip), %rax         ; @"I am a block!"
    0x100000e9f <+15>:  movq   %rdi, -0x8(%rbp)
    0x100000ea3 <+19>:  movq   %rdi, %rcx
    0x100000ea6 <+22>:  movl   %esi, -0xc(%rbp)
    0x100000ea9 <+25>:  movl   %edx, -0x10(%rbp)
    0x100000eac <+28>:  movq   %rcx, -0x18(%rbp)
->  0x100000eb0 <+32>:  movq   %rdi, -0x20(%rbp)
    0x100000eb4 <+36>:  movq   %rax, %rdi
    0x100000eb7 <+39>:  movb   $0x0, %al
    0x100000eb9 <+41>:  callq  0x100000f16               ; symbol stub for: NSLog
    0x100000ebe <+46>:  leaq   0x19b(%rip), %rcx         ; @"I am a block!"
    0x100000ec5 <+53>:  movq   %rcx, %rdi
    0x100000ec8 <+56>:  movb   $0x0, %al
    0x100000eca <+58>:  callq  0x100000f16               ; symbol stub for: NSLog
    0x100000ecf <+63>:  leaq   0x1aa(%rip), %rcx         ; @"c = %d"
    0x100000ed6 <+70>:  movl   -0xc(%rbp), %esi
    0x100000ed9 <+73>:  movq   %rcx, %rdi
    0x100000edc <+76>:  movb   $0x0, %al
    0x100000ede <+78>:  callq  0x100000f16               ; symbol stub for: NSLog
    0x100000ee3 <+83>:  leaq   0x1b6(%rip), %rcx         ; @"b = %d"
    0x100000eea <+90>:  movl   -0x10(%rbp), %esi
    0x100000eed <+93>:  movq   %rcx, %rdi
    0x100000ef0 <+96>:  movb   $0x0, %al
    0x100000ef2 <+98>:  callq  0x100000f16               ; symbol stub for: NSLog
    0x100000ef7 <+103>: leaq   0x1c2(%rip), %rcx         ; @
    0x100000efe <+110>: movq   -0x20(%rbp), %rdi
    0x100000f02 <+114>: movl   0x20(%rdi), %esi
    0x100000f05 <+117>: movq   %rcx, %rdi
    0x100000f08 <+120>: movb   $0x0, %al
    0x100000f0a <+122>: callq  0x100000f16               ; symbol stub for: NSLog
    0x100000f0f <+127>: addq   $0x20, %rsp
    0x100000f13 <+131>: popq   %rbp
    0x100000f14 <+132>: retq   

这里不用纠结汇编具体写了什么,从最右边栏的信息,我们也能大致猜测出这段汇编码应该就是block中所包裹的那段函数体实现,事实也确实如此,这里我们只需要注意第一句汇编码最左侧的0x100000e90,这个就是block中所包含的函数实现的入口地址。而刚才我们的截图中block内部所包含的FuncPtr = (void *)0x100000e90,也确实验证这个FuncPtr所指向的就是block内部所包含的那段函数实现的入口。

小结

  • block的本质是一个OC对象 因为block底层结构的第一个成员实际上是一个isa指针
  • block封装了函数调用函数调用环境。 因为block的内部存储了block函数实现的入口地址(函数调用),还捕获了block函数所用到的外部的一些变量(函数调用环境)。

Block传送门🦋🦋🦋

探寻Block的本质(2)—— 底层结构

探寻Block的本质(3)—— 基础类型的变量捕获

探寻Block的本质(4)—— Block的类型

探寻Block的本质(5)—— 对象类型的变量捕获

探寻Block的本质(6)—— __block的深入分析