学习MJ的视频课程,整理总结知识点–block详解

block是OC中实用频率很高的一个功能,同时在其它语言中也有相似的特性,比如swift中的闭包,Python中的闭包等。

本文先通过简单介绍block的使用,然后通过clang重新编译导出block的一些源码来了解block的底层结构,随后会介绍block中的重要知识点1. block中的变量捕获,2. block的类型

block的使用

在OC中使用block,大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现一个最简单的block
^{};
// 实现并调用
^{}();

// 声明并指向一个block
void (^block)(void) = ^{};
void (^block2)(int, int) = ^(int a, int b){};
// 调用
block();
block2(1, 2);

// 先声明block
void (^block3)(int);
// 实现block
block3 = ^(int a){

};
// 调用
block3(1);

我们要明白,哪些是block声明、哪些是实现、哪些是调用,使用上就不会弄混了。

block源码解析

下面我们通过一个Demo分析block的底层源码

思路是把包含block代码的文件通过clang命令重写导出,就可以看到block的源码了。

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello, World!");
};

block();
}
return 0;
}

通过clang命令重写导出为cpp文件

1
clang -rewrite-objc main.m -o main0.cpp

我们观察生成的main0.cpp文件中main函数部分,去掉里面不重要的类型转转换代码后,如下

1
2
3
4
5
6
7
8
9
10
// 定义block变量
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);

// 执行block内部的代码
// ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 因为__block_impl是第一个变量,所以和__main_block_impl_0的地址相同,所以__main_block_impl_0可以强转为__block_impl进行
block->FuncPtr(block);

__main_block_impl_0

我们从定义block变量入手分析,我们查看__main_block_impl_0,发现这一步是结构体有返回值,查看它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// c++构造函数(类似于OC的init方法),返回结构体对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

__main_block_impl_0的内部有__block_impl__main_block_desc_0、以及一个构造函数__main_block_impl_0,类似OC的init方法,通过构造函数给结构体的变量赋值,最后返回结构体。

__block_impl

接下来分析__block_impl,它的定义如下:

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

__block_impl是一个结构体,它的内部两个地方需要注意,void *isavoid *FuncPtr。isa很像之前学习的OC对象,我们知道OC对象中的isa是指向类的,我们可以提出一个疑问:block是OC对象吗,它的isa指向哪里。

另外,void *FuncPtr是个指针,它指向哪里?我们在下面的文章中再讲解这两个问题。

__main_block_func_0

回到主题,已经明白了__main_block_func_0的结构,那么接下来可以查看__main_block_func_0的的接口,如下:

1
2
3
4
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
}

这个__main_block_func_0内部其实就是我们在OC中写的下面这段代码

1
2
3
^{
NSLog(@"Hello, World!");
};

所以我们可以知道__main_block_func_0实际上就是block内执行的函数代码。

还有一个地方就是__main_block_desc_0,它的定义如下:

1
2
3
4
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

从这里我们可以了解到,__main_block_desc_0_DATA是获取block结构体的大小,其中reserved是扩展保留字。

我们回到__main_block_impl_0的构造函数,我们发现FuncPtr是指向__main_block_func_0,也是就block的执行逻辑的函数,故可以理解为FuncPtr指向block的执行函数地址。

通过block的源码学习我们可以得知:
block本质是结构体,且有isa指针(实际上block本质也是一个OC对象)
__main_block_impl_0是block的结构体的主要信息

根据block的底层源码学习,可以总结更直观的的图示,如下
block底层

block变量捕获

通过上面的雨那么分析,我们了解了block的底层结构,下面我们分析一下block中经常用到的变量捕获,同样使用上面的Demo进行分析

当在block内访问外部变量时,可能会触发变量捕获。

1
2
3
4
5
6
7
8
9
10
int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕获进来(capture)
NSLog(@"age is %d, height is %d", age, height);
// age is 10, height is 20
};
age = 20;
height = 20;
block();

根据经验我们知道,当调用block时,输出的结果为age is 10, height is 20,下面我们分析为什么会是这样,并总结规律。

首先,我们对这部分代码用clang重写导出为cpp文件,观察源码中block相关的部分,我们发现

1
2
3
4
5
6
7
8
9
10
11
12
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

我们发现,结构体__main_block_impl_0里面多了两个字段int ageint *height,其中age是数值型,height是指针型。

我们分析调用顺序

1
2
3
4
5
6
7
8
9
int age = 10;
static int height = 10;

void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
&height
);

可以发现age的值,直接被传递到__main_block_impl_0结构体中,这个结构体的构造函数会保存这个age的值,而height是指针传递,结构体中也有对应的height,但是是一个指针类型的。所以,结构体内部仅持有static类型变量的指针,当外接height的值发生改变时,block内获取到的height也会相应改变。

在block结构体内部创建ageheight保存访问的外部变量的过程,我们称之为变量捕获。变量捕获是为了保证block内部能正常访问外部变量

什么情况不需要变量捕获呢,其实我们可以推断一下,变量捕获的目的是为了保证block内部能正常访问外部的变量。假如访问的外部变量是一个全局变量,那么block随时都可以访问到这个全局变量,此时block就不需要捕获,而是直接访问这个全局变量。

总结:
block变量捕获原则
局部变量,block要访问的话就会捕获
全局变量,不会捕获,直接访问

还有一种比较常见的场景,比如在一个方法中,block访问self的属性

1
2
3
4
5
6
7
8
@implementation MJPerson
- (void)test {
void (^block)(void) = ^{
NSLog(@"-------%@", self.name);
};
block();
}
@end

此时block是够会捕获self变量或者self.name变量呢,答案是会,我们可以查看clang导出的对应源码,如下

1
2
3
4
5
6
7
8
9
10
11
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *self;
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

看到确实是捕获了,而且捕获的是self这个变量。我们可以分析一下原因:
以为self是test函数的默认参数(self和cmd),而self又不是全局变量,根据上面的总结,block访问局部变量都会进行捕获,而要访问的name属于self的对象的一个属性,所以block就不捕获self,通过self访问其中的name

block类型

我们知道block有isa,那么这个isa是否来自NSObject,block的isa是否代表它是对象类型的结构

可以先假设block是对象类型,那么调用[block class]时,必然会返回对应的类型,通过下面这段代码进行测试

1
2
3
4
5
6
7
8
void (^block)(void) = ^{
NSLog(@"Hello");
};

NSLog(@"%@", [block class]); // __NSGlobalBlock__
NSLog(@"%@", [[block class] superclass]); // __NSGlobalBlock
NSLog(@"%@", [[[block class] superclass] superclass]); // NSBlock
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]); // NSObject

通过上面代码可以确认block确实是有class的,isa指针是来自NSObject。我们可以进一步总结:

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

实际上,block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock
NSGlobalBlockNSConcreteGlobalBlock )
_NSStackBlock
NSConcreteStackBlock )
_NSMallocBlock
( _NSConcreteMallocBlock )

1
2
3
4
5
6
7
8
9
10
11
12
13
// 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存
void (^block1)(void) = ^{
NSLog(@"Hello");
};

int age = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d", age);
};

NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d", age); // __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
} class]);

我们现在知道了block有3种类型,那么它们的区别是什么?

其实从名字分析,我们就看会发现一些特点GlobalStackMalloc,实际上它们对应是在内存中的区域。如下
block所在的内存区域

程序区域:一般存放的是函数
数据区域:一般存放的是全局变量
堆区:动态分配内存,比如我们alloc出来的对象,需要开发者申请,释放对应的内存
栈区:自动分配内存局,系统自动管理,存放局部变量,函数中变量等

block存储在不同的内存区域,是根据什么来划分的?我们先给出总结的结果

block类型环境
NSGlobalBlock没有访问auto变量
NSStackBlock访问了auto变量
NSMallocBlockNSStackBlock调用了copy

实际上我们现在在项目中很少遇到__NSStackBlock__类型的block,因为在ARC环境下,__NSStackBlock__类型的block在使用的是否,系统会自动把它copy到堆上去,成为__NSMallocBlock__类型,所以我们在测试的时候,需要把ARC环境关闭(Automatic Reference Counting改为NO)才能验证


copy到堆上是为了不让程序自动管理,交由开发者管理它的释放时机

每一种类型的block调用copy后的结果如下所示
block调用copy后

关于判断存储的内存区域,我们有一个简单的方法,就是看内存地址和哪一个已知区域的地址接近

1
2
3
4
5
6
7
8
9
int age = 10;
void test() {
int a = 10;
NSLog(@"数据段:age %p", &age);
NSLog(@"数据段:class %p", [MJPerson class]);
NSLog(@"堆:obj %p", [[NSObject alloc] init]);
NSLog(@"栈:a %p", &a);
// 同一个内存区域的内存地址值比较接近
}

总结

根据本节所学的block知识,我们可以总结一下

  1. block本质上也是一个OC对象,它内部也有个isa指针
  2. block是封装了函数调用以及函数调用环境的OC对象
  3. block的底层结构如右图所示
    block底层结构

block变量捕获原则

auto变量的捕获
block底层

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型NSGlobalBlockNSConcreteGlobalBlock )_NSStackBlockNSConcreteStackBlock )_NSMallocBlock ( _NSConcreteMallocBlock )
block所在的内存区域

block类型环境
NSGlobalBlock没有访问auto变量
NSStackBlock访问了auto变量
NSMallocBlockNSStackBlock调用了copy

每一种类型的block调用copy后的结果如下所示
block调用copy后

参考和源码

Demo源码:
Block底层-Demo

评论