第三十五节—Block(二)

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

上一节重新的巩固了一下Block的基础知识和简单的使用方式,以及解决循环引用的方法,本节则将通过clang编译的文件和libclosure源码去探索Block的一些更本质的内容。

准备

1. libclosure源码

libclosure-73源码文件

2. block的.cpp文件

1. 创建一个C语言Commond Line Tool项目。
图0.2.0.png
2. 语言选择C
图0.2.1.png
3. 创建结果。
图0.2.2.png
4. 在main.c中创建一个最简单的Block对象,并且执行Block
图0.2.3.png
5. commond + B进行编译,然后打开terminal终端,进行clang编译成.cpp文件。
  • 先要进入到main.c所在的文件夹下
图0.2.4.png
  • 然后打开terminal终端,输入以下clang指令。
clang -rewrite-objc main.c -o main.cpp
6. clang结果
图1.1.6.png

一、Block的clang探索

通过clang出来的.cpp,主要探索4点。

  1. Block的本质是什么。
  2. Block()的意义。
  3. Block捕获外部变量的原理。
  4. __block的原理。

1. Block的本质

操作1 :

打开上面clang得到的main.cpp文件。滑至文件最后。

结果1 :

图1.1.0.png

操作2 :

去掉(void(*)())(void *)的强制类型转换,Block的结构就变成了

结果2 :

图1.1.1.png

操作3 :

commond + F搜索__main_block_impl_0

结果3 :

图1.1.2.png

至此,可以看到Block块的一个构造方式,其中的__block_impl结构体存储了Block块的所有信息。

操作4 :

commond + F搜索__block_impl

结果4 :

图1.1.3.png

操作5 :

根据图1.1.2图1.1.3中,__block_impl结构体的存储属性,以及官方给的注释,进入libclosureBlock_private.h。查找拥有这样结构的结构体。

结果5 :

图1.1.4.png

结论 :

block的本质是Block_layout结构体。

2. Block()的意义

操作1 :

打开刚才的.cpp文件,找到block的构造函数那一行。

结果1 :

图1.2.1.png

操作2 :

去掉block()经过clang后,得到的那行代码的所有强转符号。

结果2 :

图1.2.2.png

结论 :

1. 写在block内部的函数被保存到blockFuncPtr中,这仅仅只是函数的实现被保存进入block
2. block()才是真正的对函数进行调用。

3. Block捕获外部变量的原理

main.c中添加外部变量,更改main.c的代码为 :

#include 

int main(int argc, const char * argv[]) {
    int a = 10;
    void(^block)(void) = ^{
        printf("a = %d",a);
    };
    block();
    return 0;
}

操作1 :

  1. 重新commond + B编译main.c文件,然后进行clang转换成.cpp文件。
  2. 打开新的.cpp文件,滑至文件最后。
  3. 去掉强转。

结果1 :

图1.3.0.png

操作2 :

再次查看block的构造函数__main_block_impl_0

结果2 :

图1.3.1.png

操作3 :

查看对外部变量a的调用。

结果3 :

图1.3.2.png

结论 :

1. 当block捕获外部变量的时候,block结构体内部会生成一个相同类型、相同名称的属性。

2. 利用block自身的构造函数,将外部变量传入block结构体内部,用外部变量的值赋值给block内部自动生成的同类型元素。

3. block内部调用外部变量时,在fp函数体的内部也会生成又一个对象,对外部变量进行值拷贝,这个对象和外部变量不是同一个对象。

4. 所以,当我们对block内部捕获的外部变量进行操作的时候,是对另外一个对象进行操作,而不是对外部变量本身进行操作。

4. __block的原理

main.c中继续修改代码如下 :

#include 

int main(int argc, const char * argv[]) {
    __block int a = 10;
    void(^block)(void) = ^{
        printf("a = %d",a);
    };
    block();
    return 0;
}

操作1 :

  1. 重新commond + B编译main.c文件,然后进行clang转换成.cpp文件。
  2. 打开新的.cpp文件,滑至文件最后。
  3. 去掉强转。

结果1 :

图1.4.0.png

操作2 :

搜索__Block_byref_a_0结构体。

结果2 :

图1.4.1.png

操作3 :

查看图1.4.0中的声明blockblock的构造函数,以及block()的实现

结果3 :

图1.4.2.png

结论 :

1. __block会将外部变量变成一个结构体对象A

2. 结构体对象A内部的__forwarding存储着指向外部变量的地址的指针。

3. block结构体则在内部生成新的、该类型的结构体指针对象B

4. 通过构造函数,利用结构体对象A的指针,将结构体对象A内部的__forwarding赋值给结构体指针对象B,也就是把指向外部变量的地址的指针赋值给结构体指针对象B

5. 当调用外部变量的时候,block的函数内部会生成一个该类型的结构体指针对象C结构体指针对象C通过结构体指针对象B的赋值,拥有了指向外部变量的地址的指针。

6. 所以在block内部去改变被__block修饰的外部变量,实际上操作的是外部变量的地址上内容。

7. __block是指针拷贝的实现。

__block原理

二、Block的内存变化

在上一节Block(一)中,已经介绍了Block一共有6种类别,而开发者常用的只有其中的3种,分别是 :

1. NSGlobalBlock : 全局block
2. NSStackBlock : 栈block
3. NSMallocBlock : 堆block

1. NSGlobalBlock

先利用查看汇编,来查看NSGlobalBlock的声明和创建流程。

步骤 :

1. 随意创建一个项目,在ViewControllerviewDidLoad中,简单的创建一个最基本的block,在声明block的地方挂上断点。

- (void)viewDidLoad {

    [super viewDidLoad];
    
    void(^block)(void) = ^{    //挂上断点
        NSLog(@"this is a block");
    };
    block();

}

2. 打开xcode --> Debug --> Debug Workflow --> Always Show Disassembly,来看汇编。

图2.1.0.png

3. 使用真机进行调试。运行项目。

图2.1.1.png

4. 对objc_retainBlock加符号断点,执行到该断点位置,查看objc_retainBlock有怎样的调用。

图2.1.2.png

走到这里就不用再往后走了,看汇编的最后一句 :

图2.1.3.png

5. 此时读取x0寄存器(这也是为什么要用真机的原因,模拟器读不到x0寄存器)。

图2.1.4.png

结论 :

1. NSGlobalBlock类型的block对象,经过objc_retainBlock,调用了_Block_copy方法,最后将block对象完成创建并返回。

2. 当block没有获取外部变量的时候,block是一个NSGlobalBlock类型的block对象。当block被在声明全局变量的地方进行调用的时候,block也是NSGlobalBlock类型。

3. NSGlobalBlock存储位置在内存中的静态区(.data区)

2. NSStackBlock和NSMallocBlock

因为编译器长期处在ARC环境下,所以这两个一起说,因为不好捕捉NSStackBlock的瞬间,ARC会自动的将NSStackBlock复制到堆中,变成NSMallocBlock

步骤 :

1. 对上面NSGlobalBlock的测试代码做调整,增加一个外部变量,并在block内部捕获外部变量。

- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    int a = 10;
    
    void(^block)(void) = ^{    //挂上断点
        NSLog(@"%d",a);
    };
    block();
    
}

2. 删除之前的所有符号断点,在上述代码注释处挂上断点,执行项目

图2.2.0.png

3. 在objc_retainBlock内部查看此时的寄存器x0位置,传入的是什么类型的block

图2.2.1.png

4. 做一个objc_retainBlock符号断点,进入objc_retainBlock。一直step into__Block_copy,并在__Block_copy的最后一句汇编,也就是那句返回,挂上断点。然后再读寄存器x0

图2.2.2.png

结论 :

1. 当block捕获了外部变量之后,block的类型就从NSGlobalBlock变成了NSStackBlock

2. NSStackBlock存在于__Block_copy完成之前。

3. __Block_copy内部会将NSStackBlock变为NSMallocBlock再返回。

4. NSStackBlockNSMallocBlock的地址是不一样的,发生了一步copy的操作。也就是说,NSStackBlock通过copy可以得到NSMallocBlock

3. 为什么要对NSStackBlock进行copy

直接说结论

为了延长block的生命周期。

1. 如果block的作用域结束,那么该block就会被废弃,其内部被__block修饰的外部变量也会随之被废弃。

2. 将block从栈区复制到堆区,即使存放在栈区的block已经被废弃,堆区的block依然可以使用,被__block修饰的外部变量也不会被废弃。

图2.3.0.png
图2.3.1.png

三、Block的源码探索

1. Block结构体的解析

在上面我们已经知道了Block的本质是Block_layout结构体,这里将对这个结构体的属性做一个介绍。

图3.1.0.png

看图3.1.0,block的结构体指针中,可见5个结构体的属性。

1.1 isa

这个isa,在.cpp的文件中,赋值的是block的类型,前文说过,block一共有6种,分别是 :

1. _NSConcreteStackBlock
2. _NSConcreteMallocBlock
3. _NSConcreteAutoBlock
4. _NSConcreteFinalizingBlock
5. _NSConcreteGlobalBlock
6. _NSConcreteWeakBlockVariable

而我们常用的block类型只属于其中的3种 :

1. _NSConcreteGlobalBlock : 全局Block,对应NSGlobalBlock
2. _NSConcreteStackBlock : 栈Block,对应NSStackBlock
3. _NSConcreteMallocBlock : 堆Block,对应NSMallocBlock

1.2 flags

这是block的标识位。

  • 类型是int32_t,表明它有32bit。
  • volatile修饰,为了保证多线程操作的时候,flags永远都会从内存中读取真正的block标识数据,而不是从CPU寄存器中读取,保证数据的正确性。

flags会充分利用内存,利用bit位存储block的一些信息,类似isa中的ISA_BITFIELD利用位域存储isa信息。

flagsbit位存储的内容 :

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

第1位 : 存放着释放标记。一般利用BLOCK_NEEDS_FREE(1左移24位),做位与操作,再存入该位,记录block是否需要释放。

第16位 : 存储block的引用计数的值。

第24位 : 是否需要释放block。它会影响第1位的值,也会影响第16位的值。

第25位 : 是否拥有拷贝辅助函数。

第26位 : 是否拥有block析构函数。

第27位 : 是否有垃圾回收。

第28位 : 是否是全局变量。

第30位 : block是否拥有一个签名。如果没有签名,则第29位也不会被定义使用。

1.3 reserved

我也不知道干什么用的,如果有大佬知道的话,可以赐教,万分感谢。

1.4 invoke

看其类型——BlockInvokeFunction

typedef void(*BlockInvokeFunction)(void *, ...);

重定义类型的函数指针,也就是存储在block块内部的函数。

1.5 descriptor

block的描述信息,是结构体。拥有3种descriptor,其中,Block_descriptor_1block一定拥有的描述信息。

1.5.1 Block_descriptor_1

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

存放了一个保留值和一个block的大小。

1.5.2 Block_descriptor_2

struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

存放了一个copy方法和一个dispose方法。

官方注释 : 当flags中的BLOCK_HAS_COPY_DISPOSE,也就是flags第25位为1的时候,block就会拥有Block_descriptor_2描述。

1.5.3 Block_descriptor_3

struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

存放了block的签名和layoutlayout依赖于flags的第31位是否为1。

官方注释 : 当falgs中的BLOCK_HAS_SIGNATURE,也就是flags的第30位为1的时候,block就会拥有Block_descriptor_3

2. Block的签名

OC对象都是有签名的,既然说Block是OC对象,那么Block也必然有签名,Block签名存储在Block结构体的Block_descriptor_3属性中。

下面来验证、并且获得Block的签名。

操作1 :

  1. 随便创建一个项目,并在ViewControllerviewDidLoad中创建一个block
  2. 在下面代码注释处添加断点。
  3. 打开汇编。xcode --> Debug --> Debug Workflow --> Always Show Disassembly
  4. 连接真机,执行代码。
  5. lldb打印x0寄存器中的内容。
- (void)viewDidLoad {
    
    [super viewDidLoad];
    
    void(^block)(void) = ^{    //挂上断点
        NSLog(@"this is a block");
    };
    block();
    
}

结果1 :

图3.2.0.png

这里已经可以获得Blocktype encoding了,就是图3.2.0中的signature : "v8@?0"。关于type encoding可以看我的这片博客关于Objective-C type encoding。

其中v8表示void总共占用8位,@?表示一个不明类型的对象,0表示@?是在这个函数的第0位地址上。

那么,利用 :

[NSMethodSignature signatureWithObjCTypes:"@?"];

就可以获得Block的签名信息。

操作2 :

lldbpo [NSMethodSignature signatureWithObjCTypes:"@?"]

结果2 :

图3.2.1.png

结论 :

1. Block拥有签名,并且存储在Block_descriptor_3中。
2. Block的type encoding@?
3. Block的签名是isObject,isBlock

你可能感兴趣的:(第三十五节—Block(二))