《OC高级编程》笔记2——block的使用和实质探究
block的使用
block是什么
** block就是可以截获局部变量的匿名函数。**
解释一下:** block可以获取被定义时词法范围内的状态(比如局部变量等),并且在一定条件下(比如使用__block变量)可以修改这些状态。 **
比如,在某方法中的一个block,是可以获取到该方法内的变量的。
block的语法
block语法.jpg比如下面定义了一个:名为addBlock,参数列表是两个int型的数据,返回值也为int型的block。
int (^addBlock)(int, int) = ^(int x, int y){
return x + y;
};
int result = addBlock(2,4); // 传入实参,执行该block,返回了int型的结果。
block和其他变量一样都可以局部变量,全局变量,静态变量,甚至方法参数等。
block既然是种变量,那它也就有自己所属的类型。决定一个block是什么类型的因素是返回值和参数。int (^addBlock)(int, int)
代表返回值为int型,两个int型参数,名为addBlock。但这样表示block有点不太好。其一是阅读起来不太顺畅,其二是若我们要重构或者修改原来定义的block,则要在每个使用该block的地方进行手工修改。所以我们可以统一在一个地方对其进行类型再定义。
// 把返回值为void型,俩int型参数的block统一再定义为MyBlock类型。
typedef void (^MyBlock)(int);
...
MyBlock myBlock = ^(int x){
NSLog(@"myBlock:rereult = %d", x);
};
// 或者block作为方法参数时
- (void)doSomething:(MyBlock)myBlock param:(int)count
{
// 调用myBlock
myBlock(count);
}
注意:block的语法本身就比较怪异,再加上:定义block时(^blockName)
括号里面的是block名字,但是通过typedef
进行类型再定义时(^blockClass)
括号里表示代表该block的类型名。总之,block的语法比较别扭,别记错了。
截获局部变量
开头我们说了block是可以截获局部变量的匿名函数
。也就是说在某方法内的block是可以获取该方法定义的局部变量的。** 而且是只读的,不可以对其进行修改操作。若非要进行修改,则得在局部变量前加上__block
修饰符。**下面用三小段代码分别来验证:
// block内可以读取局部变量
int count = 10;
void (^countBlock1)(void) = ^(void){
NSLog(@"count----%d",count);
};
countBlock1();
// BlockWang[1534:689473] count----10
// 试图在block内修改局部变量
int count = 10;
void (^countBlock1)(void) = ^(void){
count++;
};
countBlock1();
上面这段代码编译时会报错:
试图在block内修改局部变量编译时报错.png
// 在局部变量前加上__block修饰符,后就可以在block内部修改此局部变量了
__block int count = 10;
void (^countBlock1)(void) = ^(void){
NSLog(@"count----%d",++count);
};
countBlock1();
// BlockWang[1534:689473] count----11
需要小心下面这段代码:我们在定义一个block后再修改了count值为2,然后再执行该block。执行的打印结果是count----10
,这就说明block“截获局部变量”的处理是在定义这个block时,而且似乎所谓“截获局部变量”就是在block中有了个和count相应的独立的数据,不然我们当修改count值时,为什么打印出的block内的该变量没变化呢?这个疑问在后面block的实现中我们慢慢分析。
int count = 10;
void (^countBlock)(void) = ^(void){
NSLog(@"count----%d",count);
};
count = 2;
countBlock(); // 执行block
// BlockWang[1534:689473] count----10
block的实质
接下来我们会把代码通过Clang命令转换为中间代码来观察block的实现,探索它的本质。
block的实现结构:
首先我们研究只打印字符串的,最简单的block:
#include "BlockClang.h"
int main()
{
void (^myBlock)(void) = ^(void){
printf("this is a block");
};
myBlock();
return 0;
}
打开终端,进入项目路径,然后敲入Clang的命令clang -rewrite-objc BlockClang.c
。此时,Finder里多了个文件BlockClang.cpp
,它正是转换后的中间代码。
小小的一段代码转换为BlockClang.cpp后竟然有超500多行,我们只提取出对我们有意义的部分:
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;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("this is a block");
}
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()
{
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
我们可以看到,block的结构实现是结构体,其中__main_block_impl_0
结构体代表block的结构。它有一个__block_impl
类型的impl
成员和__main_block_desc_0 *
类型的成员Desc
(顾名思义,它俩分别代表block的实现和描述信息)。以及一个构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
通过该构造函数,分别给block的成员赋值。那block的两个成员变量的结构又是怎么样的,它们里面都有哪些成员呢?
// __block_impld结构体的结构
struct __block_impl {
void *isa; // block的类型
int Flags; // 标志位
int Reserved; // 保留位
void *FuncPtr; // block的实现,函数指针,指向__main_block_func_0
};
// __main_block_desc_0结构体的结构
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
然后就是__main_block_desc_0
函数,即block的实现体。该函数接受一个__cself
参数,即对应的block自身。(** 思考:为什么要传一个自身作为参数? **)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("this is a block");
}
最后看main函数里block的实现和调用:
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
block为什么能截获局部变量?
我们来看个截获局部变量的block,并转换为中间代码,观察代码,以尝试解答这个问题。
int main()
{
int count = 10;
void (^myBlock)(void) = ^(void){
printf("count = %d", count);
};
myBlock();
return 0;
}
转换后的代码。只列出发生了变化的代码:
可以看到__main_block_impl_0
结构体中多了count
这个成员变量。并且构造函数的参数中也多了count这一项。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int count;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到block的实现体中__main_block_func_0
多了int count = __cself->count;
这一句。
** block之所以可以截获局部变量就是因为__cself
访问了该block里面的count成员变量,而block的count成员的值是在实现该block时赋得的。** 此时,前面我们的疑问:这个函数“为什么要传一个自身作为参数?的问题也迎刃而解,不言而喻了。”之所以该方法要传代表block结构的__main_block_impl_0
结构体为参数,就是为了读取该block捕获的局部变量。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int count = __cself->count; // bound by copy
printf("count = %d", count);
}
int main()
{
int count = 10;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
block为什么只能读取局部变量,而不能修改局部变量呢?
因为main函数中的局部变量
count
和函数__main_block_func_0
不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0
时,main函数栈还没展开完成,变量count
还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了。(不过既然如此,我们可以推断出静态局部变量之所以可以在block修改就是通过——指针。因为静态局部变量存在于内存数据段,不存在栈展开后非法访存的风险。见下一段。)
所以,对于auto类型的局部变量,不允许block进行修改是合理的。
block为什么可以又可以修改静态变量和全局变量呢?
因为它们不存在栈展开后非法访存的风险。所以可以通过** 指针 ** 来传递静态变量的。
可以看出静态变量在main内实现block时,捕获的是count
的地址&count
。以及在__main_block_impl_0
结构体中成员变量变成了指针类型int *count;
。即通过指针修改(它们是址传递)。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *count;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_count, int flags=0) : count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *count = __cself->count; // bound by copy
printf("count = %d", ++(*count));
}
int main()
{
static int count = 10;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &count));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
为什么被__block修饰的局部变量在block中却又是可以修改的?
我们来一段局部变量前加了__block
的代码例子:
#include "BlockClang.h"
int main()
{
__block int count = 10;
void (^myBlock)(void) = ^(void){
printf("count = %d",++count);
};
myBlock();
}
转换中间代码后,看到比以前多了很多东西。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
printf("count = %d",++(count->__forwarding->count));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}
int main()
{
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 10};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
可以看到__main_block_impl_0
结构体成员变量count
变为了__Block_byref_count_0 *
类型。而相应的__main_block_func_0
函数中count
也变为了__Block_byref_count_0 *
类型。
__Block_byref_count_0
也是一个结构体。它的构成是:
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding; // 指向另外一个变量,这儿的具体实现思路不太懂
int __flags;
int __size;
int count;
};
** 但是问题照样存在,我们修改的变量count
它是位于栈上的。若当block被回调执行时,栈早已被展开,早没count
了。这该如何是好?**
上面的代码中我们可以注意到:__main_block_desc_0
函数中多了两个成员函数,分别指向__main_block_copy_0
,__main_block_dispose_0
函数。
当block从栈上被copy到堆上时,会调用
__main_block_copy_0
将__block
类型的成员变量count
从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0
来释放__block
类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,__forwarding
的作用就体现出来了:当一个__block
变量从栈上被复制到堆上时,栈上的那个__Block_byref_i_0
结构体中的__forwarding
指针也会指向堆上的结构。
资料参考:
iOS中block实现的探究
C语言中闭包的探究及比较
C语言中闭包的探究及比较
对Objective-C中Block的追探
谈Objective-C block的实现