前言
大家在日常的开发工作中经常会用到Block,都知道它是一个匿名函数,那具体是一个怎样的结构呢,相信知道的人不多,今天我们重点查看下Block的底层源码实现原理。
一、Clang分析Block
示例👇
- (void)viewDidLoad {
[super viewDidLoad];
int a = 10;
void (^block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
}
我们先clang看看,block所对应的c++代码是什么样的👇
我们先看看函数名称_block_impl_0
接着我们看看匿名函数的第1个入参__ViewController__viewDidLoad_block_func_0
👇
第2个入参__ViewController__viewDidLoad_block_desc_0_DATA
👇
至此,我们通过Clang分析Block对应的底层C++代码,Get到两点:
- Block对应的C++底层是结构体
xxx_block_impl_0
,其中xxx
就是Block所在的路径
,哪个文件的哪个方法里声明的Block。该结构体包含2个重要的入参:block_func_0
和_block_desc_0
。(block_desc_0_DATA
只是_block_desc_0
的一个别名) - Block会自动捕获外部变量,并将其保存到了Block底层结构体中。
至于block_func_0
和_block_desc_0
的底层源码,我们在后面会有分析。
现在我们稍微改一下示例代码, 将int a
改成__block int a
👇
- (void)viewDidLoad {
[super viewDidLoad];
__block int a = 10;
void (^block)(void) = ^{
NSLog(@"Cooci - %d",a);
};
NSLog(@"%@",block);
}
再看看clang之后的结果👇
明显发现,由a
变成了(__Block_byref_a_0 *)&a
。
接着看看__Block_byref_a_0
的定义👇
然后查看a的赋值
,在__ViewController__viewDidLoad_block_func_0
里面👇
由之前的int a = __cself->a
变成__Block_byref_a_0 *a = __cself->a
。之前是值拷贝
,现在是引用的拷贝
,a是一个地址指针,指向了外部变量a=10,使用的时候取的是a->__forwarding->a
,就是引用
。
最后看看Block的底层xxx_block_impl_0
👇
和类
一样,它也有isa指针
,指向_NSConcreteStackBlock
,说明Block也分类别,具体分为哪几类呢?
1.1 Block的类型
Block大致分为3种类型:_NSConcreteStackBlock
_NSConcreteGlobalBlock
和 _NSConcreteMallocBlock
,具体区别如下👇
类别 | 存储域 | 详情 |
---|---|---|
_NSConcreteStackBlock | 栈区 |
自动截获变量 并且在该变量作用域内
|
_NSConcreteGlobalBlock | 静态全局区域(.data区) | 在定义全局变量的地方 定义Block;Block语法的表达式中不截获任何变量时 ,或只截获了全局变量、静态变量
|
_NSConcreteMallocBlock | 堆区 | 当_NSConcreteStackBlock 超出变量作用域,ARC大多数情况下,编译器进行适当判断后调用_Block_copy 拷贝到堆 上 |
示例👇
- _NSConcreteStackBlock 栈区Block,注意:现在必须使用
__weak
修饰,
int a = 10;
void ( __weak ^block)(void) = ^{
NSLog(@"----%d",a);
};
// block_copy
NSLog(@"%@",block);
打印结果是
<__NSStackBlock__: 0x7ffeed6d53f8>
- _NSConcreteGlobalBlock全局Block
void (^block)(void) = ^{
NSLog(@"------");
};
NSLog(@"%@",block);
打印结果是
<__NSGlobalBlock__: 0x10bb2d030>
- _NSConcreteMallocBlock堆区Block
int a = 10;
void (^block)(void) = ^{
NSLog(@"----%d",a);
};
NSLog(@"%@",block);
打印结果是
<__NSMallocBlock__: 0x60000179d950>
全局区的Block
很好理解,不包含任务外部变量,要包含也只能是全局变量或静态变量,说白了就是静态全局区的变量。但是栈区 和 堆区的,就很难区分了,在ARC
下编译器大多数情况
会适当地进行判断然后,自动将Block从栈复制到堆
,那编译器在什么情况下不能判断
需要手动复制
呢?
向方法或函数的
参数中传递Block
时
但是如果在方法或函数中适当地复制了
传递过来的参数,那么就不必
在调用该方法或函数前手动复制
了,以下方法或函数不用手动复制:
- Cocoa框架的方法且方法名中含有
usingBlock
等时 - GCD的API
下图是变量作用域
在被 Block
和 __block修饰符
作用后生命周期发生的变化👇
示例👇
typedef void (^blk_t)(void);
NSArray *getBlockArray() {
int val = 10;
//ARC不会自动复制,需手动复制
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0: %d", val);}, ^{NSLog(@"blk1: %d", val);}, nil];
// return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy],nil];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^globalBlock)(void) = ^{ };
//__NSGlobalBlock__
NSLog(@"GlobalBlock is %@", globalBlock);
__block int a = 10;
void (^stackBlock)(void) = ^void { a++; };
//MRC __NSStackBlock__
NSLog(@"StackBlock is %@", stackBlock);
//ARC __NSMallocBlock__
NSLog(@"MallocBlock is %@", stackBlock);
NSArray *array = getBlockArray();
blk_t blk = (blk_t)[array objectAtIndex:0];
![2020111314240475.png](https://img.haomeiwen.com/i3444487/a06867d70cb7ad2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
blk(); //如果没有手动copy复制,崩溃。因getBlockArray()执行完后,栈上的Block被废弃。
}
return 0;
}
1.2 __block变量存储域
捕获了__block修饰符
的外部变量的Block
,__block变量
的存储域
也会受到影响
。
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
1.2.1 持有情况
如下图所示👇
- 在
一个Block
中使用了__block变量
- 在
多个Block
中使用了__block变量
1.2.2 释放情况
堆上Block被废弃,它所使用的__block变量也就被释放
1.3 __forwarding
之前我们看到了如果Block持有了外部被__block修饰的变量,那么C++底层会将外部变量做一个引用处理,使用到了__forwarding
👇
可以看出,__forwarding实际是指向了结构体__Block_byref_a_0的首地址,如下图所示👇
那么,__block变量被Block从栈区copy到堆区后,Block的成员变量__forwarding的指向也发生了变化,如下图所示👇
1.4 Copy 和 Dispose
还是回到我们之前clang生成的C++源码,有两个函数👇
一个copy
,一个dispose
,为什么会有这两个函数的存在呢?因为C语言结构体不能含有附有__strong修饰符
的变量,因为编译器不知道应何时运行C语言结构体
的初始化
和废弃
操作,不能很好地管理内存。但是OC的运行时库
能准确把握从栈复制到堆
以及堆上的Block被废弃
的时机
,因此Block结构体中即使含有附有__strong修饰符
或__weak修饰符
的变量,也可以恰当地进行初始化和废弃。为此需要使用在xxx_block_desc_0
结构体中增加的成员变量copy
和dispose
,以及作为指针赋值给该成员变量的__main_block_copy_0函数
和__main_block_dispose_0函数
。
1.4.1 栈copy到堆的时机
既然copy
操作是将栈上的Block复制到堆时,那什么时候会触发这个copy
操作呢?
- 调用Block的
copy
实例方法时 - Block作为
函数返回值
返回时 - 将Block赋值给附有
__strong修饰符id类型
的类或Block类型成员变量
时 - 在方法名中
含有usingBlock的Cocoa框架方法
或GCD的API
中传递Block时
1.4.2 基础类型 与 对象类型 被Block捕获的区别
分两种情况:不包含__block修饰符 和 __block修饰符下的
- 不包含__block修饰符
-
基础类型的👇 --> 结构很简单,直接使用int a,并没有
copy
和dispose
函数
-
对象类型的👇 (将
int a = 10;
改为NSNumber *a = @(10);
) -->使用的是对象 cself->a,有copy
和dispose
函数,__flag
是3(BLOCK_FIELD_IS_OBJECT
)
-
- __block修饰符下的
- 基础类型的👇(将
int a = 10;
改为__block int a = 10;
)
--> a的类型是引用类型结构体__Block_byref_a_0
,使用a是__forwarding->a
,同时包含copy
和dispose
函数,__flag
是8(BLOCK_FIELD_IS_BYREF
)
- 对象类型的👇 (将
int a = 10;
改为__block NSNumber *a = @(10);
)
--> Block中包含成员变量void (*__Block_byref_id_object_copy)(void*, void*);
和void (*__Block_byref_id_object_dispose)(void*);
, a的类型也是__Block_byref_a_0
,使用a也是__forwarding->a
,__flag
也是8(BLOCK_FIELD_IS_BYREF
)
综上,得出下表👇
外界变量类型 | 被Block捕获后变量的类型 | 包含copy dispose函数 | __flag标识位 |
---|---|---|---|
基础类型 | 原类型 | 否 | 默认标识 |
对象类型 | 原类型 | 是 | BLOCK_FIELD_IS_OBJECT |
__block基础类型 |
__Block_byref_a_0 引用类型,不包含成员变量copy dispose
|
是 | BLOCK_FIELD_IS_BYREF |
__block对象类型 |
__Block_byref_a_0 引用类型,包含成员变量copy dispose
|
是 | BLOCK_FIELD_IS_BYREF |
二、循环引用
2.1 什么是循环引用
示例代码👇
warning :Capturing 'self' strongly in this block is likely to lead to a retain cycle
2.2 解决方式
打破循环引用有3种方式:
- 我们大家熟知的,
weak-strong-dance
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.name = @"lg_cooci";
self.block = ^(void){
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@", strongSelf.name);
};
self.block();
}
切记,一定要
__strong
,防止Block捕获的对象过早的释放。
- __block修饰的外部变量持有
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"lg_cooci";
__block ViewController *vc = self;
self.block = ^(void){
NSLog(@"%@", vc.name);
vc = nil;
};
self.block();
}
注意,__block外部变量使用完成后,记得
置为nil
,否则引发内存泄露。
3.Block中添加入参
typedef void(^KCBlock)(ViewController *);
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"lg_cooci";
self.block = ^(ViewController *vc){
NSLog(@"%@", vc.name);
};
self.block(self);
}
这种方式代码里最少,也很好理解。
三、Block底层原理
之前我们只是从C++代码的层面
,大致分析了Block的底层结构体组成,再结合__block对外部变量存储域的影响,现在我们再从汇编层
入手,查看Block的底层实现流程。
首先断点进入汇编层👇
再在objc_retainBlock
处打断点,step into进入查看👇
发现了Block是在库libobjc
,底层是调用_Block_copy
,就是我们通常所说的从栈拷贝到堆
,这个会在后面的三层Copy中重点分析其内部流程处理。
Block签名
接着我们在block调用前打断点,再查看寄存器信息👇
Block本身就是匿名函数
,当然有方法签名
信息。@
代表对象,?
代表是函数指针。
Block底层源码
Block对应的底层是结构体Block_layout
,其中flags
是个枚举,用来描述Block对象的👇
部分注解如下👇
- 第1 位,释放标记,一般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags , 告知该 block 可释放。
- 第16位,存储引用计数的值,是一个可选用参数;
- 第24位,程序根据它来决定是否增加或是减少引用计数位的值;
- 第25位,是否拥有拷贝辅助函数(a copy helper function);
- 第26位,是否拥有 block 析构函数;
- 第27位,标志是否有垃圾回收;
- 第28位,标志是否是全局block;
- 第30位,与 BLOCK_USE_STRET 相对,判断是否当前 block 拥有一个签名。用于 runtime 时动态调用。
同时,注意到Block_layout
还包含一个结构体Block_descriptor_1
👇
除了Block_descriptor_1
之外,并未包含Block_descriptor_2
和Block_descriptor_3
,这是因为在没有引用外部变量
或捕获到不同类型变量
时,编译器会改变结构体的结构,按需
添加Block_descriptor_2
和Block_descriptor_3
。
当flags
包含BLOCK_HAS_COPY_DISPOSE
时,会加入Block_descriptor_2
;当flags包含BLOCK_HAS_SIGNATURE
时,会加入Block_descriptor_3
。Block_descriptor_2
和Block_descriptor_3
是通过Block_descriptor_1
的指针偏移
来访问的。
Block 结构图如下所示👇
__block底层源码
之前我们通过clang得出的C++源码可知,被__block修饰符修饰的外部变量,在Block中是结构体Block_byref
👇
除了Block_byref
,还有Block_byref_2
Block_byref_3
,道理与Block_descriptor_x
一样,也是编译器根据__block修饰的变量的类型
来确定的。其中Block_byref_2
里的byref_keep
和 byref_destroy
函数是来处理里面持有对象的保持
和销毁
。
重点:三层Copy
- 第一层Copy
之前我们分析过,Block在引用外部变量
的情况下是栈block
,但是通过变量作用域的改变
就变成堆block
,通过汇编我们也得出,是经过了objc_retainBlock
,它又调用了_Block_copy
👇
- 第二层Copy
_Block_copy
源码的最后,执行了_Block_call_copy_helper
,源码如下👇
继续看看_Block_descriptor_2
源码👇
其实是通过flags中是否有BLOCK_HAS_COPY_DISPOSE
值来判断是否需要copy、dispose
,然后通过内存平移,找到对应的Block_descriptor_2
返回。
如果满足Block_descriptor_2
👇
里面包含一个成员copy
,根据我们上面分析知道,__block修饰的对象类型
,被Block捕获后,会生成成员变量copy dispose,我们示例看下底层C++代码,查找下对应的copy函数👇
- (void)viewDidLoad {
[super viewDidLoad];
__block NSNumber *a = @(10);
void (^block)(void) = ^{
NSLog(@"Cooci - %@", a);
};
block();
NSLog(@"%@",block);
}
clang一下👇
最终定位到是_Block_object_assign
,看其源码👇
接着看_Block_byref_copy
源码👇
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src指向栈
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
//这里或上4是因为栈的forwarding通过下面的代码指向了堆
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // 堆上的forwarding指向堆自身
src->forwarding = copy; // 栈上的forwarding也指向堆
copy->size = src->size;
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
//有copy、dispose,通过src偏移一个struct Block_byref结构体大小拿到src2, 也就是包含copy和dispose成员变量的Block_byref_2结构体
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
//从src2偏移一个struct Block_byref_2大小拿到src3, 也就是包含layout成员变量的Block_byref_3结构体
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
//调用外部的__Block_byref_id_object_copy_131
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
我们看到struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
这里就是第二层copy。
从 copy->forwarding = copy; src->forwarding = copy;
这两句代码可以看出,堆上的变量的forwarding指向了自己,而栈上的forwarding也指向了堆,这样就实现了栈和堆指向同一变量
的操作,也就是__block为什么可以修改持有的外部变量的原因。
- 第三层Copy
在_Block_byref_copy
中我们看到src2->byref_keep
,其实就是调用外部的__Block_byref_id_object_copy_131
,为什么?
__Block_byref_id_object_copy_131
入参里面,有一个内存平移40,原因👇
而131 = 128 +3,其中128表示BLOCK_BYREF_CALLER --> 代表__block变量有copy/dispose的内存管理辅助函数👇
我们示例里是对象类型NSNumber,就表示是这个BLOCK_FIELD_IS_OBJECT
,即为3。然后copy函数拼起来就是__Block_byref_id_object_copy_131
,而它里面调用的是_Block_object_assign
,走的就是下面这个case👇
这个就是最后一层的copy操作。
Block销毁流程
最后我们看看Block的销毁流程是什么样的。上述示例中,我们去查查dispose里调用的底层方法是哪个?👇
131的原因同理,不做分析了。继续看_Block_object_dispose
👇
示例的变量类型是__block NSNumber
类型,所以走case BLOCK_FIELD_IS_BYREF
,接着看看_Block_byref_release
👇
因为Block捕获的外部变量是__block NSNumber
类型,存在一个栈copy到堆
的过程,所以需要释放堆的Block,通过byref->forwarding
找到堆Block
,最终free
释放掉。
其它的情况:基础类型
,对象类型
,被__block修饰的基础类型
,也是按照这个思路跟进源码去分析流程,这里就不做分析。
总结
本篇文章开头通过Clang得出Block对应的底层结构体,同时配合__block修饰符
分析了基础类型
、对象类型
、被__block修饰的基础类型
和被__block修饰的对象类型
四种情况下的变量的存储域的变化
,还有所对应的底层Block的类别
,及Block的成员变量和函数
的区别,进而分析了3层copy
和dispose销毁
的流程。
网友评论