Block的三次探索
Block是带有自动变量的匿名函数。
匿名函数的含义是Block没有函数名,另外Block带有插入记号“^”,插入记号便于查找到Block。
一般形式的Block:
^int (int cs) {
return cs * 2;
};
如果Block没有参数,参数列表可省略,形式如下:
^int{
return 666;
};//参数列表的括号也可以省略
在C语言中,函数返回值类型省略则默认为int类型;在Block中也可以省略返回值类型,不过与C语言函数不同,Block省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,没有return语句就使用void类型。
返回值为void类型时的Block:
^{
printf("iam Block”);
};
返回值为int类型时的Block:
^{
return 666;
};
上面定义了Block语句块,要想使用还需要Block类型变量:
int (^block1)(int) = ^int (int cs) {
return cs * 2;
};
printf("%d",block1(666));
声明Block类型变量:
int (^block2)(int);
Block类型变量可以作为自动变量、函数参数、静态变量、全局变量使用。以函数参数为例:
int test(int (^block1)(int)) {
return 1;
}
除了看起来长一点也没啥,typedef来解决_,
typedef int (^Block)(int);
int test(MyBlock block1) {
return 1;
}
这样就舒服了、看起来。
接下来就是高潮部分了,就是”带有自动变量“以及如何带有自动变量?
int var1 = 10;
int var2 = 20;
void (^block3)(void) = ^{
printf("%d",var1);
};
var1 = 20;
block3();
情况1:打印结果是10,而不是20。
情况2:在Block中修改var1的值会报错。
情况3:如果修改的是Objective-C对象,例如NSArray对象,不可以对其赋值但是可以增删元素。
情况4:对于全局变量、静态全局变量、静态变量在块中可以修改。
情况5:使用__block修饰var1变量后,打印的结果是20,而且var1可以在块中被修改。
另外注意:不能在block中访问C语言字符数组,但是可以访问C语言字符串,也就是说Block并没有截获字符数组。
解释
要想明白为什么会出现上述情况,我们必须深入了解Block,打开终端进入项目main.m所在的文件夹,使用命令clang -rewrite-objc main.m
,执行之后该文件夹下会出现一个main.cpp的C++文件,文件中代码很长,我们只需看重要的一些代码。
这是一个没有使用局部变量的block:
#import <Foundation/Foundation.h>
int main() {
void (^block)(void) = ^{
printf("block\n");
};
block();
return 0;
}
在main.cpp中我们看这些代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};//^^^^结构体2^^^^
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;
}
};//^^^^结构体1^^^^
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block\n");
}
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)};
//^^^^结构体3^^^^
int main() {
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
有三个结构体(代码中已标记)我们必须铭记于心,或许你看到这里就不想往下看了,但是你对一个知识点的理解深度与面试官对你的好感成正比,Block几乎每个iOS面试官都会问到。
结构体1:
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;
}
};
这个结构体包含结构体2和结构体3,另外还有一些在该block中使用到的局部变量,最后是一个构造函数,对结构体的成员(上述结构2、结构3、局部变量)进行初始化。请注意构造函数的第一个参数,下面还会说。
结构体2:
struct __block_impl {
void *isa; //block也是对象
int Flags; //标志
int Reserved; //版本升级所需的区域
void *FuncPtr; //函数指针
};
结构体3:
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)};
然后会发现上面还有这个函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block\n");
}
对于上面这段代码是不是很熟悉,很明显对应于我们定义的block,也就是Block使用的匿名函数被转换为了C语言函数,命名的根据Block所在的函数名和出现的顺序。需要注意的是还有一个struct __main_block_impl_0 *__cself
指针,为什么要这样设计Block呢?结合类、对象、方法、自己思考。
上面说注意结构体1中构造函数的第一个参数,现在看下main函数中void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
就是用上面的函数的地址作为参数的。
第二个参数时作为静态全局变量传入的,在结构体3最后可以看到使用__main_block_impl_0
的大小进行初始化的。
这段代码就是将栈上生成的变量赋值给block变量。
上面看了最简单的Block的转换代码,下面来看一个使用到了局部变量的block:
#import <Foundation/Foundation.h>
int main() {
int var1 = 10;
int var2 = 10;
void (^block)(void) = ^{
printf("%d\n", var1);
};
block();
return 0;
}
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int var1;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _var1, int flags=0) : var1(_var1) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在结构体1中加入了使用到的局部变量。现在就可以解释情况1-3了:
情况一和情况二解释:自动变量会以值传递的方式拷贝到Block的结构体中,因为并没有传递自动变量的地址,所以不能修改自动变量的值。
情况三解释,修改NSArray对象可以,重新赋值则不行,因为修改对象并没有改变对象指针。
下面再来说说__block
说明符:
通过上面的方法可以对非静态全局变量、静态全局变量、静态局部变量、__block
变量进行测试,可以得到:
情况四解释:对于访问全局变量和静态全局变量,对于Block的结构体没有任何影响,因为其地址是不变的、作用域也足够广。对于静态局部变量,虽然地址是唯一的,但是Block超出了其作用域,所以将静态局部变量的指针传递给了Block的结构体。
为什么不将自动变量的地址拷贝到Block中呢?
自动变量超出作用域之后会被废弃,而Block可能被拷贝到堆上。所以我们看下__block
变量的实现。
struct __Block_byref_var1_0 {
void *__isa;
__Block_byref_var1_0 *__forwarding;
int __flags;
int __size;
int var1;
};
变换后变成了结构体。
其初始化为:
__attribute__((__blocks__(byref))) __Block_byref_var1_0 var1 = {(void*)0,(__Block_byref_var1_0 *)&var1, 0, sizeof(__Block_byref_var1_0), 10};
从初始化可以看出:__forwarding
指针指向自己(不一定,后面会说),var1变量相当于原自动变量。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_var1_0 *var1 = __cself->var1; // bound by ref
printf("%d\n", (var1->__forwarding->var1));
}
访问var1时为什么兜圈子呢?后面会说。
另外还有结构体2中的isa指针,我们知道在类与对象中:对象的isa指针指向所属的类,类的isa指针指向元类,元类的isa指针指向根元类,根元类的isa指针指向自己。
而Block的isa指针的指向有三种,而且我们暂时给出它们的特点:
NSConcreteStackBlock:存储在栈上的的Block。特点:使用了局部变量,可以被赋值到堆上。
NSConcreteMallocBlck:存储在堆上的BlocK。特点:由NSConcreteStackBlock拷贝到堆上,持有对象。
NSConcreteGlobalBlock:存储在程序的数据区。特点:不使用局部变量,整个程序中只需一个实例。
为什么,第一个事例没有使用局部变量,isa指针却:
impl.isa = &_NSConcreteStackBlock;
虽然说isa指针指向的是_NSConcreteStackBlock,但它的实现是NSConcreteGlobalBlock,因为它没有使用局部变量。
设置在栈上的Block超出作用域会被废弃,__block
也会被废弃。在以下几种情况Block会被复制到堆上:
1、调用copy方法(_NSConcreteStackBlock调用copy方法会从栈复制到堆,_NSConcreteGlobalBlock什么也不做,_NSConcreteMallocBlock引用计数加一);
2、作为函数返回值;
3、将Blcok赋值给类的成员变量时;
4、向方法名中含有usingBlock的Cocoa框架方法或GCD的API传递Block时。
而__block变量
会跟随Block从栈复制到堆、并被Block持有。栈上的__blcok
变量的__forwarding
指针也指向堆上的结构体,保证了可以访问同一个__block
变量。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->var1, (void*)src->var1, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->var1, 8/*BLOCK_FIELD_IS_BYREF*/);}
这两个函数就是保证__block
变量被复制到堆上被Block持有和释放。
这个实例中我们使用__block
修饰的int类型,如果修饰的对象呢?
对象有四种修饰符:__strong、__weak、__unsafe_unretained、__autorelealeasing
四种修饰符,对于````__strong同样是使用上述的两个函数进行持有和释放。而对于
__weak修饰的对象,当超出作用域后被废弃,也就是即使同时使用
__block __weak修饰了对象,但是当对象废弃后,Block持有的对象也会变成nil。同理
__unsafe_unretained需要注意悬垂指针的问题。最后同时使用
__block __autoreleasing```会编译错误。
上面还曾说过一句话:堆上的Block持有对象,现在说完:堆上的对象会持有__strong
对象,而栈上的Blcok不持有。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
当栈上的Block复制到堆上时,会调用这copy函数持有对象,使用dispose函数释放对象。
Blcok通过参数区分copy和dispose函数的对象类型是对象还是__block
变量。
最后就是Block的循环引用:
循环引用这里就不在细说,解决方式其一就是使用__weak
修饰符来解决。
现在主要说一下另外一种方式:
使用__block
变量手动置nil:
__block id tmp = self;
_myblock = ^{
[tmp doSomething];
tmp = nil;
};
但是需要注意此Blcok必须要的执行
最后补充:我们在将Blcok从栈copy到堆上时都是使用copy,这是因为栈上的Block使用retain是无效的,只有使用copy函数可以。但是对于在堆上的Block可以通过copy和retain持有,所以还是推荐使用copy。
对于Block的探索,这不是第一次也绝对不是最后一次。