第三十五节—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 <stdio.h>

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 <stdio.h>

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 1 Block机制 (Very Good) Block技巧与底层解析 http://www.jianshu.com...
    Kevin_Junbaozi阅读 4,030评论 3 48
  • 本文的内容主要是基于Clang编译器的官方文档所写。 在开始探索Block的本质之前,大家先试着分析一下,下面的代...
    无忘无往阅读 827评论 0 2
  • 本文主要介绍block的类型、循环引用的解决方法以及block底层的分析 block 类型 block主要有三种类...
    北京_小海阅读 594评论 0 2
  • 手动目录循环引用block的类Block的相关信息block本质block如何捕获外界变量?__block修饰的本...
    Engandend阅读 362评论 0 1
  • 一 Block的实现 1. 在main函数中声明、实现并调用一个block 2. 然后我们通过clang命令将ma...
    TIGER_XXXX阅读 478评论 0 0