block,一个用的熟的不能再熟的东东。但是,被问起block是怎么实现的,为什么要使用___weak,为什么会造成循环引用,__block怎么用,block分为几种。。。我就傻眼了。这次,就好好来分析一下,从本质出发~
1 - Block的声明以及实现
首先block的申明 (返回数据类型)(^block名称)(参数);
如下代码:
@property (copy , nonatomic) void(^blockName)(void);
// 或者如下
void(^tempHandle)(void);
block的实现 ^返回数据类型(参数){具体部分}
如下代码:
^int(int b) {
return 1;
};
// 省略返回值和参数代码如下
^ {
return 1;
};
2 - Block的本质
根据之前我们学过的方法,我们先创建一个简单的包含block的代码,如下。
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
bool(^gloadBlock)(int a) = ^bool(int b){
NSLog(@"--- block");
return false;
};
NSLog(@"%d",gloadBlock(1));
}
return 0;
}
使用命令指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
得到对应的cpp文件,main.cpp。
main.cpp中包含了以下代码
关于main函数如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_24b9e7_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
}
return 0;
}
关于__main_block_impl_0的数据结构:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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;
}
};
关于__block_impl的数据结构:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
关于__main_block_desc_0的数据结构:
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)};
来总结一下上面的结论。
block,就是一个结构体指针
(也是一个oc的对象),它的结构体包含有两个属性impl
和Desc
。
impl
是一个结构体__block_impl
,它包含有4个属性,如下表格:
属性名 | 内容 |
---|---|
isa | 一个isa指针,关于isa指针,参照第一篇文章 |
Flags | FIXME: 这里未完成 |
Reserved | FIXME: 这里未完成 |
FuncPtr | block包含函数调用以及函数调用环境 |
3 - Block的变量捕获
我们再来看下面的这个block代码:
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
int intA = 123;
bool(^gloadBlock)(int a) = ^bool(int b){
NSLog(@"--- %d",intA);
return false;
};
intA = 321;
NSLog(@"%d",gloadBlock(1));
}
return 0;
}
对于这个block,输出结果是
2021-02-08 17:20:49.357112+0800 Block_01[29634:735842] --- 123
2021-02-08 17:20:49.357532+0800 Block_01[29634:735842] 0
Program ended with exit code: 0
这个结果,也许和你想的不太一样。为什么不是--- 321
。
再比较下下面的代码:
int intA;
int main(int argc, const char * argv[]) {
@autoreleasepool {
intA = 123;
bool(^gloadBlock)(int a) = ^bool(int b){
NSLog(@"--- %d",intA);
return false;
};
intA = 321;
NSLog(@"%d",gloadBlock(1));
}
return 0;
}
输出结果是
2021-02-08 17:25:36.922729+0800 Block_01[29653:738966] --- 321
2021-02-08 17:25:36.923093+0800 Block_01[29653:738966] 0
Program ended with exit code: 0
对于这样的问题。我们应该分析下底层代码来找寻答案。
使用命令导出c++的代码分别如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int intA = 123;
bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intA));
intA = 321;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_470d12_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
}
return 0;
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
intA = 123;
bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
intA = 321;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_aec0d3_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
}
return 0;
}
注意这句代码:
bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intA));
__main_block_impl_0
构造函数后面多了一个参数intA
。
再这个结构体__main_block_impl_0
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int intA;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intA, int flags=0) : intA(_intA) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
该结构体中多出了 int intA
这个属性,并且构造函数intA(_intA)
可知,再创建这个block的时候,将在内部创建一个intA
属性,将block外部intA
的值传入结构体中。所以,在block之外改变intA
的值,并不会改变block内部的intA
的值。
而将intA
放入全局变量的话,block中并不会存在intA
属性。
这就是block的变量捕获~
我们先来看一下block的变量捕获。
变量类型 | 是否捕获 | 访问方式 |
---|---|---|
局部变量(auto) | ✅ | 值传递 |
局部变量(static) | ✅ | 指针传递 |
全局变量 | ❌ | 直接访问 |
我们来校验一下上面的结论,运行以下代码:
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int intA = 123;
auto int intB = 123;
bool(^gloadBlock)(int a) = ^bool(int b){
NSLog(@"--- %d,%d",intA,intB);
return false;
};
intA = 321;
intB = 321;
NSLog(@"%d",gloadBlock(1));
}
return 0;
}
输出结果:
--- 321,123
0
我们再来看一下转换成c++的代码吧。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int intA = 123;
auto int intB = 123;
bool(*gloadBlock)(int a) = ((bool (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &intA, intB));
intA = 321;
intB = 321;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_swf2m0ds52l_g_ly5nz8vxmw0000gn_T_main_dfee55_mi_1,((bool (*)(__block_impl *, int))((__block_impl *)gloadBlock)->FuncPtr)((__block_impl *)gloadBlock, 1));
}
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *intA;
int intB;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_intA, int _intB, int flags=0) : intA(_intA), intB(_intB) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
当局部变量是auto修饰的话(默认为auto),block会进行值捕获。当局部变量是static修饰的话,block会进行指针捕获。self属于auto修饰的局部变量
。
4 - Block的类型
先看一下下面的代码:
#import
void (^gloadBlockC)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
static int intA = 123;
auto int intB = 123;
// GlobalBlock
void(^globalBlock)(int a) = ^void(int b){
NSLog(@"--- %d,",b);
};
void(^globalBlockB)(void) = ^void(){
NSLog(@"--- %d,",intA);
};
gloadBlockC = ^void(){
};
NSLog(@"%@",[globalBlock class]);
NSLog(@"%@",[globalBlockB class]);
NSLog(@"%@",[gloadBlockC class]);
NSLog(@"%@",[^void(){
} class]);
// StackBlock
NSLog(@"%@",[^void(){
NSLog(@"%d",intB);
} class]);
// MallocBlock
void(^mallocBlock)(void) = ^void(){
NSLog(@"--- %d,",intB);
};
NSLog(@"%@",[mallocBlock class]);
}
return 0;
}
在ARC环境下
运行结果为:
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSStackBlock
NSMallocBlock
在MRC环境下
运行结果为:
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSGlobalBlock
NSStackBlock
NSStackBlock
所以,block有3种类型:global、stack、malloc。
为什么第六个block类型在ARC和MRC会不一样?经过测试验证,将__NSStackBlock__做copy后,得到的就是一个__NSMallocBlock__
.所以,应该是ARC环境下,NSStackBlock的赋值会自动加入copy操作造成的。
我们总结一下:
block类型 | 产生的环境 | 存储区域 | copy之后的效果 |
---|---|---|---|
GlobalBlock | block没有访问auto变量 | 数据区域 | 类型不变 |
StackBlock | block访问了auto变量 | 栈 | 从栈赋值到堆 |
MallocBlock | StackBlock进行了copy | 堆 | 引用计数+1 |
经查询资料,补充一个知识点:
在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
- block作为函数返回值时
- 将block赋值给
__strong
指针时 - block作为Cocoa API中方法名含有
usingBlock
的方法参数时 - block作为GCD API的方法参数时
为了自己来管理block的释放,我们应该将属性的block放入堆中。所以,在ARC下,利用上述第二条特性,block用strong
或者copy
来修饰。而在MRC下,block就用copy
来修饰。