欢迎大家关注我的个人博客:https://darkknightkazuma.github.io
一、Block基础介绍
1、概念介绍
Block又称为块或块对象,它是苹果在OSX10.6和iOS4.0中新加入的功能,是C语言层面的特性及功能实现,类似其它语言的闭包(closure)功能.当时苹果正使用LLVM的clang作为C语言的编译器来改进C/OC/C++/OC++等的编译处理,Block也是当时的重要成果.
##2、块的定义及语法
Block是带有自动变量(局部变量)的匿名函数.因为底层结构体实现有isa指针,也被看作是块对象.Block的出现其实是为了代替指针函数的一种语法结构,之前传递状态需要使用不透明的void指针来传递状态,而Block把C语言特性所编写的代码封装成简明且易用的接口.
下面是Block的实现语法结构:
block的语法是脱字符^加花括号,花括号里是块的实现.块可以当成是变量的值使用.如下:
^{
//Block implementation
}
Block类型的声明语法结构:
return_type (^block_name)(parameters)
//中间的block_name可以看作是变量名
Block的调用:
block_name(parameters)
二、Block的关于捕获变量的底层实现
在块声明的范围里,所有变量都可以为其所捕获.捕获的自动变量不可修改,修改要加__block前缀.
块总能修改实例变、静态变量、全局变量、全局静态变量,无需加__block.
我们经常会看到block相关的文章或面试题中有这些内容.那么Block底层实现是怎么样的呢?
#####1、我们首先来看一下Block的内存布局,如下:
/* Revised new layout. */
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; //指向Class对象
int flags; //状态标志位
int reserved; //预留内存大小
void (*invoke)(void *, ...); //指向块实现的函数指针
struct Block_descriptor *descriptor;
/* Imported variables. */
};
其中最重要的就是invoke函数指针和descriptor块的描述.invoke函数指针它指向了块的实现,它的void*参数传入的是块的结构体. descriptor的结构体中包含了块大小以及两个重要的辅助函数指针等.我们注意到块的布局中,最下面一部分是捕获到的变量,前面提到的void*参数传入块的结构体也是为了拿到捕获到的变量.那么下面让我们来看一下块的实际源码是什么样的:
cd到目录下,在终端通过 clang -rewrite-objc 文件名 的方法将文件转换为C/C++代码.如果发现不能转换需要下载插件,指令为xcode-select --install.
int main(int argc, const char * argv[]) {
@autoreleasepool {
int ivar = 1;
void (^tmpBlock)(void) = ^{
NSLog(@"tmpBlock:%d",ivar);
};
tmpBlock();
}
return 0;
}
转换后点开目录下的.cpp文件,会看到上万行的代码,屏蔽掉无用的代码,直接找到主要代码如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int ivar;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _ivar, int flags=0) : ivar(_ivar) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int ivar = __cself->ivar; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_459ed6_mi_2,ivar);
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int ivar = 1;
void (*tmpBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, ivar));
((void (*)(__block_impl *))((__block_impl *)tmpBlock)->FuncPtr)((__block_impl *)tmpBlock);
}
return 0;
}
我们来挨个看一下:
__block_impl 结构体是包含我们刚刚在Block布局中提到的前四个成员变量.
__main_block_impl_0结构体是包含了__block_impl结构体变量和__main_block_desc_0结构体指针以及捕获到的变量.最后是初始化构造函数中对impl成员和desc进行赋值.
__main_block_func_0这个函数他传递了__main_block_impl_0*型的参数,并从中读取块结构体中捕获的参数进行实际操作.看到这里你有个明白了这个就是对应上面那个invoke函数指针所指向的函数.
__main_block_desc_0这个结构体就是上面所说的块的描述结构体.可能你会发现他少了两个辅助函数的函数指针,原因后面会说.
最后是main函数中的具体初始化和函数调用.刚好对应块变量的声明实现部分和块的调用.
#####2、关于捕获自动变量的分析
捕获自动变量这部分首先我们要明确有哪几种变量,如下:
- auto自动变量:默认方法的作用域中不加前缀就是自动变量,而且ARC下默认还会加__strong.
- static静态变量:存放在内存的可读写区,初始化只在第一次执行时起作用,运行过程中语句块变量将保持上一次执行的值.
- static全局静态变量:也放在可读写区,作用域为当前文件.
- 全局变量:也在可读写区,整个程序可用.
- 另外在OC中它们又分为对象类型和非对象类型.
下面让我们看一下他们在Block中的表现.
首先是前面那个是用自动变量ivar的例子,如果我们修改它的值编译器会报警告:
Variable is not assignable (missing __block type specifier)
提示我们不能修改变量,这是为什么呢?我们来打印一下block内外的指针变量的地址:
int ivar = 1;
NSLog(@"块外%p",&ivar);
void (^tmpBlock)(void) = ^{
NSLog(@"块内%p",&ivar);
};
tmpBlock();
2019-12-05 18:20:40.063273+0800 LearningDemo[69358:5789849] 块外0x7ffeefbff4dc
2019-12-05 18:20:40.063840+0800 LearningDemo[69358:5789849] 块内0x103400ad0
你会发现它们不是同一个地址,是的,block的__main_block_impl_0结构体拷贝了一份自动变量进去作为结构体的成员变量,你修改的是结构体内部的ivar的值,而不是外部ivar的值,他们并不是同一块内存上的东西.苹果让编译器在这种情况下报警告,提示开发者是改不了的.
接下来我们给它加一个static前缀,如下:
static int ivar = 1;
NSLog(@"块外%p",&ivar);
void (^tmpBlock)(void) = ^{
ivar = 2;
NSLog(@"块内%p",&ivar);
};
tmpBlock();
2019-12-05 18:41:36.083804+0800 LearningDemo[69801:5806136] 块外0x100003320
2019-12-05 18:41:36.084770+0800 LearningDemo[69801:5806136] 块内0x100003320
有意思的事情发生了,可以修改变量了,而且地址居然一样了~那我们看一下转换后的源码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *ivar;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_ivar, int flags=0) : ivar(_ivar) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *ivar = __cself->ivar; // bound by copy
(*ivar) = 2;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_0279cbtj42s5p4r1j0yxvfmc0000gn_T_main_532557_mi_3,&(*ivar));
}
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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
static int ivar = 1;
NSLog((NSString *)&am