文章目录
概述
什么是Blocks?
Blocks是C语言的扩充功能。用一句话来表示Blocks
的扩充功能:带有自动变量(局部变量)的匿名函数。
但是在C语言的标准中不允许存在匿名函数。通过Blocks
,源代码中就可以使用匿名函数。
Blocks模式
Block语法
完整形式的Block
语法与一般的C语言函数定义相比,仅有两点不同:
- 没有函数名
- 返回值类型前带有“^”(插入记号)。
语法:
^ 返回值类型 参数列表 表达式
“返回值类型”同C语言函数的返回值类型,“参数列表”同C语言的参数列表,“表达式”同C语言函数允许使用的表达式。表达式中含有return
语句时,其类型必须与返回值类型相同。
例如:
^ int (int count)(return count + 1;)
Block语法会省略返回值类型以及参数列表,变成下面这样:
^ 表达式
Block类型变量
在Block
语法下,可将Block语法赋值给声明为Block类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。Blocks中由Block语法生成的值也被称为“Block”。
声明Block类型变量的示例如下:
int (^blk)(int)
该Block类型变量与一般的C语言变量完全相同,可作为一下用途:
- 自动变量
- 函数参数
- 静态变量
- 静态全局变量
- 全局变量
下面我们试着使用Block赋值为Block类型变量:
int (^blk)(int) = ^(int count)(return count + 1;);
由“^”开始的Block语法生成的Block被赋值给变量blk中,因为与通常的变量相同,所以也可以由Block类型变量向Block类型变量赋值。
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
在函数参数中使用Block类型变量可以向函数传递Block
:
void func((^blk)(int))
在函数返回值中指定Block类型,可以将Block
作为函数的返回值返回。
int (^func())(int) {
return ^(int count)(return count + 1;);
}
从上面代码可以看出,在函数参数和返回值中使用Block
类型变量时,记述方式复杂。这时可以利用typedef
来解决该问题:
typedef int (^blk_t)(int);
原来的记述方式就变为:
void func(blk_t blk)
blk_t func()
通过Block
类型变量调用Block
与C语言通常的函数调用没有区别。在函数参数中使用Block
类型变量并在执行Block
的例子如下:
int func(blk_t blk, int rate) {
return blk(rate);
}
当然,在Objective-C
的方法中也可使用:
- (int) methodUsingBlock:(blk_t)blk rate:(int)rate {
return blk(rate);
}
Block
类型变量可完全像通常的C语言变量一样,因此也可以使用指向Block
类型变量的指针,即Block
的指针类型变量。
typedef int (^blk_t)(int);
blk_t blk = ^(int count)(return count + 1;);
blk_t *blkptr = &blk;
(*blkptr)(10);
由此可知,Block
类型变量可像C语言中其他类型变量一样使用。
截获自动变量值
举例来说明:
在该源代码中,Block
语法的表达式使用的是它之前的自动变量fmt
和val
。Blocks中,Block
表达式截获所使用的自动变量的值,即保存该变量值的瞬间值。因为Block
表达式保存了自动变量的值,所以在执行Block
语法后,即使改写Block
中使用的自动变量的值也不会影响Block
执行时自动变量的值。该源代码就在Block
语法后改写了Block
中的自动变量val
和fmt
。
下面是执行结果:
val = 10
执行结果并不是改写后的值“These values were changed. val = 2
”,而是执行Block
语法时的自动变量的瞬间值。该Block
语法在执行时,字符串指针“val = %d\n
”被赋值到自动变量fmt
中,int
值10被赋值到自动变量val
中,因此这些值被保存(即被截获),从而在执行块时使用。
这就是自动变量值的截获。
__block说明符
实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能改写此值。我们来尝试改写截获的自动变量值:
int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n", val);
以上是在Block语法外声明的给自动变量赋值的源代码。该源代码会产生编译错误:
若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符。该源代码中,如果给自动变量声明int val附加__block说明符,就能实现Block内赋值。
__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n", val);
该源代码的执行结果为:
val = 1;
使用附有__block说明符的自动变量可在Block中赋值,该变量称为__block变量。
截获的自动变量
如果将值赋值给Blcok中截获的自动变量,就会产生编译错误。
int val = 0;
void (^blk)(void) = ^(val = 1;);
该源代码会产生以下编译错误:
但是截获Objective-C对象,调用变更该对象的方法是不会产生编译错误:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
但是若向截获的变量array
赋值则会产生编译错误。该源代码中截获的变量为NSMutableArray
类的对象。如果用C语言来描述,就是截获NSMutableArray
类对象用的结构体实例指针。虽然赋值给截获的自动变量array
的操作会产生编译错误,但使用截获的值却不会有任何问题。下面源代码向捕获的自动变量进行赋值,会产生编译错误:
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
这种情况下,需要给截获的自动变量附加__block
说明符。
__block id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
另外,在使用C语言数组时必须小心使用其指针。源代码示例如下:
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
只是使用C语言的字符串字面量数组,而并没有向截获的自动变量赋值,因此看似没有问题。但实际上会产生以下编译错误:
这是因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获。这时,使用指针可以解决该问题:
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n", text[2]);
};
Blocks的实现
Block实质
参考博客:【一篇文章剖析block底层源码以及Block.private】
通过代码去分析一下Block语法转换成的C++代码:
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
//经过clang转换后的C++代码
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("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)};
int main(int argc, const char * argv[]) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
转化后的C++源码很复杂,所以我们分成几个部分来看。
- 首先是__block_impl结构体
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
- 结构体的名称:
impl
即implementation
的缩写,换句话说这一部分是block
的实现部分结构体。 - void *isa:声明一个不确定类型的指针,用于保存Block结构体实例。
- int Flags:标识符。
- int Reserved:今后版本升级所需的区域大小。
- void *FuncPtr:函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。
- 在介绍
struct __main_block_impl_0
结构体之前,先介绍一下static struct __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)};
- 第一个成员变量指的是今后版本升级所需区域的大小(一般填0)。
- 第二个成员变量是
Block
的大小。 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
:- 这就是和我们平时用结构体一样,在定义完最后写一个结构体实例变量,变量名就是
__main_block_desc_0_DATA
。 - 其中
reserved
为0,Block_size
是sizeof(struct __main_block_impl_0)
。
- 这就是和我们平时用结构体一样,在定义完最后写一个结构体实例变量,变量名就是
- 然后是
struct __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;
}
};
- 第一个成员变量是impl,也就是上面点1中的结构体的变量。
- 第二个成员变量是Desc指针,就是上面点2中的结构体的指针。
- 剩下的代码就是:初始化含有这些结构体的
__main_block_impl_0
结构体的构造函数。
- 接下来就是
static void __main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
printf("Block\n");
}
这一部分就是Blcok
执行的实际代码块。也是点3中fp
指针指向的函数。括号中的参数__cself
是相当于C语言版的self
,代表的是Block
本身。
- 最后来看main函数
int main(int argc, const char * argv[]) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
先看第一行代码,这部分代码就是定义和初始化部分。
因为转换较多,所以看着很麻烦,去掉转换的部分,具体如下:
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
该源代码将__main_block_impl_0
结构体类型的自动变量,也就是栈上生成的__main_block_impl_0
结构体实例的指针,赋值给__main_block_impl_0
结构体指针类型的变量blk。
第二行代码就是相当于源代码中的blk()
,即使用该Block部分。去掉转换部分就是:
(*blk->impl.FuncPtr)(blk);
这是使用函数指针调用函数。由Block语法转换的__main_block_impl_0
函数的指针被赋值成员变量FunPtr
中。
以上就是Block
的实质,Block即为Objective-C
对象。
截获自动变量
源代码:
int main(int argc, const char * argv[]) {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
blk();
return 0;
}
和上面一样,将截获自动变量值的源代码通过clang进行转换:
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;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
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[]) {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
return 0;
}
这次与上次不同的是在Block语法表达式中使用的自动变量被当作成员变量追加到了__main_block_impl_0
结构体中:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
}
初始化该结构体实例的构造函数也发生了变化
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val)
初始化时自动变量fmt和val进行了赋值操作:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;
由此可知,在__main_block_impl_0
结构体实例中(即Block),自动变量被截获。
再看一下使用Block的匿名函数的实现,
源代码:
^{printf(fmt, val)};
转换为下面:
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}
在转换后的代码中,截获到__main_block_impl_0
结构体实例的成员变量上的自动变量,这些变量在Block语法表达式之前被声明定义,所以之后即使改变自动变量的值也不会对Block语法中的内容有所变化。
__block说明符
像前面我们说的一样,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出被截获自动变量赋值的操作时,就会报错。那这样就无法在Block中保存值了,极为不便。这时我们就可以使用“__block说明符”,更准确的表述是“__block存储域类说明符”。
下面用代码来介绍__block的作用:
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^blk)(void) = ^{
val = 1;
printf("val = %d\n", val);
};
blk();
return 0;
}
经clang变换后的代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
printf("val = %d\n", (val->__forwarding->val));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src {
_Block_object_dispose((void*)src->val, BLOCK_FIELD_IS_BYREF);
}
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
};
int main(int argc, const char * argv[]) {
__Block_byref_val_0 val = {
0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
还是逐个去分析:
__Block_byref_val_0
结构体
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
其他几个成员变量没什么说的,主要看一下__Block_byref_val_0 *__forwarding
,这个相当于指向该结构体本身的一个指针。
__main_block_impl_0
结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这部分值得注意的是,对于我们的__Block_byref_val_0
结构体,我们同样是用一个指针去保存,这么做的原因是通过__block
修饰的变量可能会被不止一个block
使用,使用指针可以保证其可以被多个block
调用。
static void __main_block_func_0
函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
printf("val = %d\n", (val->__forwarding->val));
}
这里需要注意的是,对val赋值的时候需要通过forwarding
指针。
- 最后再看一下主函数:
int main(int argc, const char * argv[]) {
__Block_byref_val_0 val = {
0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__main_block_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}
主要看一下__block修饰的变量的赋值:
__Block_byref_val_0 val = {
0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
这个__block变量val变为了__Block_byref_val_0
结构体变量。通过调用 static void __main_block_func_0
函数(通过__Block_byref_val_0
结构体成员变量__forwarding访问成员变量val),将10赋给val。
Block存储域
在上文出现过_NSConcreteStackBlock
一词,这是描述Block对象的类。分为以下三种:
_NSConcreteStackBlock
:该类的对象Block设置在栈上。_NSConcreteGlobalBlock
:该类的对象Block设置在数据区域
(.data)中。_NSConcreteMallocBlock
:该类的对象则设置在由malloc函数分配的内存块中(即堆
)。
在以上例子中的Block都是_NSConcreteStackBlock
类,且都设置在栈上,但实际上并不都是这样,在记述全局变量的地方使用Block语法时,生成的Block对象就是_NSConcreteGlobalBlock
;以及Block语法的表达式中不使用应截获的自动变量时,生成的Block对象也是_NSConcreteGlobalBlock
。不会有一创建的Block对象就是分配在堆上的,但是可以对栈上的Block对象copy就可以实现分配在堆上。
但是,当我们不知道Block对象在哪个存储域时,且使用了copy方法,这样会如何呢?
不管Block
配置在何处,用copy
方法复制都不会引起任何问题,即使在不确定情况下也可以调用copy
方法。
__block变量存储域
上面讲了Block的存储域,下面再看一下__block变量的存储域。
使用__block变量的Block从栈上复制到堆上时,__block变量也会受到影响。
-
当1个Block中使用__block变量,则当该block从栈复制到堆上时,使用的所有的这些__block对象也全部从栈复制到堆,此时Block持有__block对象。
-
在多个
Block
中使用__block
变量时,因为最先会将所有的Block
配置在栈上,所以__block
变量也会配置在栈上。在任何一个Block
从栈复制到堆上时,__block
变量也会一并从栈复制到堆上并被该Block所持有。当剩下的Block
从栈复制到堆上时,被复制的Block
持有__block
变量,并增加到__block
变量的引用计数。
- 如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。
此思考方式和Objective-C
的引用计数式内存管理完全相同。使用__block
变量的Block
持有__block
变量。如果Block
被废弃,则它所持有的__block
变量也被释放。
现在我们理解了__block
变量的存储域之后,可以回过头想想上面的使用__block
变量用结构体成员变量__forwarding
的原因。“不管__block
变量配置在栈上还是堆上,都能够正确的访问该变量”,就像这句话所说,通过Block
的复制,__block
变量也从栈上复制到堆上,此时可同时访问栈上的__block
变量和堆上的__block
变量。
通过该功能,无论是在Block
语法中、Block
语法外使用__block
变量,还是在栈上或堆上,都可以顺利地访问同一个__block
变量。
截获对象
总结一下栈上的Block
会复制到堆上的情况:
- 调用Block的
copy
实例方法 - Block作为函数返回值返回时
- 在Block赋值给附有
__strong
修饰符id
类型的类或Block
类型成员变量时 - 在方法名中含有
usingBlock
的Cocoa
框架方法或Grand Central Dispatch
的API中传递Block
时
介绍两种函数,也就是在上文中介绍__block
说明符时未说明的C++源代码中的__main_block_dispose_0
(以下简称dispose
函数)和__main_block_copy_0
函数(以下简称copy
函数)。前者相当于release
实例方法,后者相当于copy
实例方法。
当栈上的Block被复制到堆上时,可以归结为_Block_copy
函数被调用时Block从栈上复制到堆上,同样的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose
函数,这相当于对象的dealloc
实例方法。有了这种构造,我们就可以通过使用附有__strong
修饰符的自动变量,使Block中截获的对象就能够超出其变量作用域而存在。
Block中使用的赋值给附有__strong
修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因而可以超出其变量作用域而存在。
只有调用_Block_copy
函数才能持有截获的附有__strong修饰符的对象类型的自动变量值,如果不调用_Block_copy
函数,即使被截获了,也会随着变量作用域的结束被废弃。
因此Block中使用对象类型自动变量时,除以下情形外,推荐使用Block的copy实例方法:
- Block作为函数返回值返回时
- 将Block赋值给类的附有__strong修饰符的id类型或Block类型成员变量时
- 在方法名中含有
usingBlock
的Cocoa
框架方法或Grand Central Dispatch
的API中传递Block
时
因为以上三种情形下,不使用copy实例方法,栈上的Block也会复制到堆上。
__block变量和对象
用__block修饰对象:
__block id obj = [[NSObject alloc] init];
通过clang转换为:
先介绍一个函数:_Block_object_assign
:该函数相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
通过调用_Block_object_assign函数和_Block_object_dispose函数,控制赋值给附有__strong修饰符的对象类型自动变量的对象的持有和释放状态。
我们前面用到的只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符呢?
首先是在Block中使用附有__weak修饰符的id类型变量的情况:
执行结果为:
这是由于附有__strong修饰符的变量array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的变量array2中。
若同时指定__block修饰符和__weak修饰符会怎么样呢?
这是因为即使附加了__block说明符,附有__strong
修饰符的变量array也会在该变量作用域结束的同时被释放被废弃,nil被赋值给附有__weak
修饰符的变量的array2中。
不要同时使用__autoreleasing
修饰符与__block
说明符,会产生编译错误。
Block循环引用
如果在Block中使用附有__strong
修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环引用。
当该对象被Block持有,而Block又被该对象持有,就会产生循环引用。使用__weak或者__block修饰该对象即可解决。
使用__weak修饰符的实质是将对象的强引用改为弱引用,使用__block修饰符实质是在Block中将__block变量赋nil值。
参考博客:[iOS开发]Block