Block-深入分析笔记

前言

block是日常iOS开发高频率使用的闭包,之前也看过不少文章,但是一直疏于总结,今日再次深入研究一下,并记录其过程。

Block结构定义

通过clang编译,去除block的相关嵌套(可以去除的原因是因为结构体本身并不带有任何额外的附加信息),得到如下结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

分析可知,block实际包含6部分:

  • isa指针:同所有OC对象,指向Block的所属类型。这里也证明block的本质其实是对象。通过循环打印其superClass,得到的结果链为:
1
NSGlobalBlock -> __NSGlobalBlock -> NSBlock -> NSObject -> null
  • flags:用于按 bit 位表示一些 block 的附加信息。
  • reserved:保留变量。
  • invoke:函数指针,指向具体的 block 实现的函数调用地址。
  • descriptor:Block_descriptor类型的结构体,描述block的附加信息、size大小、copy、dispose的指针。
  • variables:捕获的变量。

Block源码实现

为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是:

1
clang -rewrite-objc 文件地址

one. 未捕获外界变量

我们在main.m文件中写如下代码:

1
2
3
4
void testBlock(){
void(^myBlock)(void) = ^{};
myBlock();
}

编译生成main.cpp,内部核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//block的结构
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;//指向调用函数的地址
};

//主要函数
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;//block描述信息
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//block的实现。{}里面的代码。
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
}

//block描述信息
static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
void testBlock(){
void(*myBlock)(void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}

通过主要函数切入分析:

1、如下,由于clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以打印的为栈block。开启ARC后、如果没有捕获外界变量,则为全局block,反之为堆block

1
impl.isa = &_NSConcreteStackBlock;

2、impl为函数指针,指向此处的_testBlock_block_func_0,代表block内部的函数实现。

3、desc__testBlock_block_desc_0结构体,代表当前block的附加信息,包括结构体大小,需要copy和dispose的变量列表等。

  • copy:当block从栈copy到堆中时,其内部的变量以及捕获的外界变量将调用这个copy函数对其进行retain操作进行持有
  • dispose:当block跳出其所在的作用域不再被外界持有时候也即是当其需要被释放之时,将调用dispose函数进行将内部的变量和捕获变量进行relese.

4、flags:标记这个block此时的状态,因为C语言是静态语言,无法获取block此时的状态,那么就难以去管理其的生命周期,这里的flag就是标记其此时的状态.这里的flag其实不止是对其生命周期有管理,也有存储了里面的函数签名,是否全局等信息。

two. 复制外界变量

我们通过修改main.m文件的代码,增加变量a。

1
2
3
4
5
6
7
8
#include <stdio.h>
void testBlock(){
int a = 10;
void(^myBlock)(void) = ^{
printf("a = %d\n",a);
};
myBlock();
}

如下可见,函数实现的地方发生了变化,其中增加了一个变量a。其实这个变量是在声明block时,被复制到函数中的。所以当修改函数内部a的值,并不会影响到外部值的变化。

1
2
3
4
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("a = %d\n",a);
}

three. __block捕获外界变量

捕获的本质意义是因为作用域的问题,当涉及到跨函数访问的时候,便需要捕获。而全局变量一直在内存中,所以不需要捕获。

通过修改main.m中的代码,给a添加__block关键字修饰,并且在函数内部修改实现。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
void testBlock(){
__block int a = 10;
void(^myBlock)(void) = ^{
printf("a = %d\n",a);
a = 11;
};
myBlock();
printf("a = %d\n",a);
}

编译得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//保存捕获的外部变量。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};

struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;//clang编译下的都是stackBlock
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("a = %d\n",(a->__forwarding->a));
(a->__forwarding->a) = 11;
}
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __testBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __testBlock_block_impl_0*, struct __testBlock_block_impl_0*);
void (*dispose)(struct __testBlock_block_impl_0*);
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0), __testBlock_block_copy_0, __testBlock_block_dispose_0};
void testBlock(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*myBlock)(void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
printf("a = %d\n",(a.__forwarding->a));
}

分析可知:

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的。

1、增加了__Block_byref_a_0结构体,用来保存我们需要捕获的外部变量。结构体的中值有:

1
2
3
4
5
6
7
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
  • __isa:用来包装这个结构体的.通过这个isa可以区分它的具体类型。表明这里也是一个对象。
  • __forwarding:指向自身,block安全取值的指针。取值方式如下:
1
2
3
4
5
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
printf("a = %d\n",(a->__forwarding->a));
(a->__forwarding->a) = 11;
}

这样取值的原因是因为:

a是个局部变量保存在栈区,而block是结构体在堆区,试想如果a在方法执行完毕后那么原值会被系统释放,此时如果block被调用如果是直接a->a(即是访问上图中结构体里的int a,这个int a 就是保存的外界的栈中的a),那么a能取到值吗?肯定不可以的。

forwarding指针的作用就是标记当block被copy到堆中,在捕获的外界变量被block标记后,forwarding指针就会将其原本指向栈中变量的地址转为指向堆中的a结构体,此时里面的a就会转为堆里的a.所以通过a->forwarding->a去取值是能够保证正常取到a的.

这也就是为什么block标记后就能修改原a,而不进行这样标记就无法修改值,因为c语言里的a是基本变量,基本变量的传值是值传递.而被block进行捕获后已经转为了对象,拷贝进堆区后传到block内的其实是在堆区的a地址,这里的传值是地址传递!

  • __size:标记结构体所占大小。
  • __flags:同其它结构体,为标志位常量。

Block类型分析

block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void testBlockClass(){
int age = 1;
void (^block1)(void) = ^{
NSLog(@"block1");
};

void (^block2)(void) = ^{
NSLog(@"block2:%d",age);
};

NSLog(@"%@/%@/%@",[block1 class],[block2 class],[^{
NSLog(@"block3:%d",age);
} class]);
}

通过打印得到三种不同block类型:

1
__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__

__NSConcreteStackBlock

顾名思义,栈block放在栈区。通过在ARC环境下,给某个文件添加-fno-objc-arc标识,使文件能在MRC环境下运行。

在block中有捕获外界变量的则为_NSConcreteStackBlock

1
2
3
4
5
__block int a = 10;
void(^myBlock)(void) = ^{
printf("a = %d\n",a);
};
NSLog(@"%@",[myBlock class]);

__NSMallocBlock

顾名思义,堆block放在堆区。在MRC环境下,继续上面代码,block中如果捕获了外界变量,此时通过打印[myBlock copy],得到的结果便是__NSMallocBlock。这里再次copy,便只是增加了堆block的引用计数而已。

值得注意的是如果在ARC中,此时只要捕获外界变量,那么就直接是MallocBlock,因为ARC下会自动将这个栈block copy到堆中。

__NSGlobalBlock

全局block存放在数据区。如果block未捕获外界变量,则为__NSGlobalBlock,这时对block进行copy将不再发生任何变化。

Block常见问题

我们日常在使用block的时候也会遇到各种问题,通过上文的底层剖析,对一系列问题来进行原理性解释,更加巩固知识记忆。

Q1:确定下面代码的输出值
1
2
3
4
5
6
int age=10;
void (^Block)(void) = ^{
NSLog(@"age:%d",age);
};
age = 20;
Block();

根据上面two. 复制外界变量介绍可知,block在声明时就将外部变量复制进来,所以之后外部变量值的更改不会影响内部的值。所以这里打印依旧是10.

Q2:确定下面代码的输出值
1
2
3
4
5
6
7
8
int age = 10;
static int num = 25;
void (^Block)(void) = ^{
NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

这里考察了外部变量修改是否对内部造成影响,因为age到block中是值拷贝,同上文,所以外部的修改不会影响内部的打印。而static变量为指针传递,所以会影响。

变量age可能会自动销毁的,内存可能会小时,所以不能采用指针拷贝。而num一直在内存中。

综上,这里的打印的结果是10,11。

Q3:block访问self是否需要捕获?

会,self是当调用block函数的参数,参数是局部变量,self指向调用者。

Q4:block访问成员变量是否需要捕获?

会,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量

Q5:在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上的几种情况?
  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时
Q6:当block内部访问了对象类型的auto变量时,是否会强引用?

如果block在空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象。

如果block在空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用。

这里主要分析堆block

1、如果block被拷贝到堆上。

会调用block内部的copy函数 -> 函数内部调用_Block_object_assign -> _Block_object_assign根据变量原有修饰符(strong、weak、__unsafe_unretained)作出相关操作,来辨别是否采用强弱引用。(注意:这里仅限于ARC时会retain,MRC时不会retain)。

2、如果block从堆上移除

会调用block内部的dispose函数 -> 函数内部调用_Block_object_dispose -> dispose函数会自动释放引用的auto变量。

Q7:weak 在使用clang转换OC为C++代码时,可能会遇到以下问题:
cannot create __weak reference in file using manual reference

此时需要更改编译指令,支持ARC、指定运行时系统版本,如:

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
Q8:判断gcd引用的对象何时销毁

1、直接引用。

1
2
3
4
5
6
Person *person = [[Person alloc] init];
person.age = 10;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"age:%d",person.age);
});
NSLog(@"touchesBegan");

打印结果:

1
touchesBegan ——> age:10 ——> Person-dealloc

分析:gcd的block默认也会做copy操作,这里的block为堆block,会对person对象强引用。直到block销毁时对象才销毁。

2、__weak引用。

1
2
3
4
5
6
7
Person *person = [[Person alloc] init];
person.age = 10;
__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"age:%p",weakPerson);
});
NSLog(@"touchesBegan");

打印结果:

1
touchesBegan --> Person-dealloc --> age:0x0

这里道理很简单,block内部弱引用,当外部函数作用域结束后,person便销毁了。所以内部打印为空。

3、双重gcd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person *person = [[Person alloc] init];
person.age = 10;

__weak Person *weakPerson = person;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2-----age:%p",person);
});
NSLog(@"1-----age:%p",weakPerson);
});

NSLog(@"touchesBegan");

打印结果:

1
touchesBegan -> 1-----age:0x604000015eb0 -> 2-----age:0x604000015eb0 -> Person-dealloc

分析:只要内部有强引用,都会等到强引用部分打印完后才会释放。如果弱引用部分迟于强引用部分执行,那弱引用部分打印为空。

Q9:block内部可以向可变数组添加元素吗?

答案是可以的。如果可变数组已经生成,这时它在内存中的地址便可以确定,添加元素只是使用数组指针,并未发生修改。

Q10:被__block修饰的对象类型在block上如何操作的?

1、当__block变量在栈上、不会对指向的对象产生强引用。

2、copy到堆中时、结论如上文Q6.

Q11:如何循环引用?

这恐怕是我们日常开发中最经常遇到的问题了,因为循环引用所造成的内存泄漏致使内存只涨不跌。解决问题大致有以下三种方式:

1、__weak __strong:这里的__strong为了防止vc的提前释放,导致block内部使用的vc为空,因此通过强引用使vc的生命周期也能通过block内部管理。

1
2
3
4
5
__weak ViewController *weakSelf = self;
void(^myBlock)(void) = ^{
__strong ViewController *strongSelf = weakSelf;
NSLog(@"name == %@",strongSelf.name);
};

2、通过__block:这里需要在block里面当不需要使用vc的引用属性的时候要在生命作用域里手动将其置空,并至少保证要将其调用一次。

1
2
3
4
5
6
__block ViewController *blockSelf = self;
void(^myBlock)(void) = ^{
NSLog(@"name == %@",blockSelf.name);
blockSelf = nil;
};
myBlock();

3、将VC传参:前面有提过block也是匿名函数,如果将vc作为形参传入block中,那么其vc指针的生命周期就只在block内部,当block执行完毕其指针就会被释放,也就是说,其引用次数也会被-1,block持有vc的引用链就会被断掉,也就不存在循环引用问题了。

1
2
3
4
void(^myBlock)(ViewController *vc) = ^{
NSLog(@"name == %@",vc.name);
};
myBlock(self);

其它更多问题参考block本质,本文只是摘取了部分。后续有新的block疑问可以添加到此文中。

参考链接

1、唐巧-谈Objective-C block的实现

2、https://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/