Blocks是C语言的扩充功能。用一句话来表示Blocks
的扩充功能:带有自动变量(局部变量)的匿名函数。
但是在C语言的标准中不允许存在匿名函数。通过Blocks
,源代码中就可以使用匿名函数。
完整形式的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(^)(int)) func {
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语法瞬间的值。保存后就不能改写此值。我们来尝试改写截获的自动变量值:
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
是相当于OC语言版的self
,代表的是Block
本身。
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捕获的外部变量可以改变值的是静态变量、静态全局变量、全局变量,const修饰的变量,个人觉得没什么不同,const修饰的全局变量和平时全局变量一样,修饰的局部变量和普通的局部变量一样。Block是不捕获全局变量的,直接访问;捕获的是局部变量,局部自动变量是通过地址传递,而局部静态变量是通过值传递。
变量类型 | 是否能捕获 | 传递方式 |
---|---|---|
全局变量 | 否 | 直接访问 |
局部自动变量(auto) | 是 | 值传递 |
局部静态变量(static) | 是 | 指针传递 |
像前面我们说的一样,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出被截获自动变量赋值的 *** 作时,就会报错。那这样就无法在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。
在上文出现过_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变量也会受到影响。
当1个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
会复制到堆上的情况:
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类型变量的情况:
ARC环境下执行结果为:
若同时指定__block修饰符和__weak修饰符会怎么样呢?
这是因为即使附加了__block说明符,附有__strong
修饰符的变量array也会在该变量作用域结束的同时被释放被废弃,nil被赋值给附有__weak
修饰符的变量的array2中。
不要同时使用__autoreleasing
修饰符与__block
说明符,会产生编译错误。
如果在Block中使用附有__strong
修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环引用。
当该对象被Block持有,而Block又被该对象持有,就会产生循环引用。使用__weak或者__block修饰该对象即可解决。
使用__weak修饰符的实质是将对象的强引用改为弱引用,使用__block修饰符实质是在Block中将__block变量赋nil值。
对于非ARC下, 为了防止循环引用, 我们使用__block来修饰在Block中使用的对象:
对于ARC下, 为了防止循环引用, 我们使用__weak来修饰在Block中使用的对象。原理就是:ARC中,Block中如果引用了__strong修饰符的自动变量,则相当于Block对该变量的引用计数+1。
iOS Block在ARC/非ARC下的使用总结
参考博客:[iOS开发]Block
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)