一、Blocks 概要
什么是Blocks Blocks是C语言的扩充功能。可以用一句话来表示 Blocks的扩充功能,带有自动变量(局部变量)的匿名函数。 顾名思义,所谓匿名函数就是不带有名称的函数。C语言的标准不允许存在这样的函数。例如以下源代码∶
int func (int count);
它声明了名称为 func的函数。下面的源代码中为了调用该函数,必须使用该函数的名称func。
int result = func(10);
如果像下面这样,使用函数指针来代替直接调用函数,那么似乎不用知道函数名也能够使用该函数。
int result =(*funcptr)(10);
但其实使用函数指针也仍然需要知道函数名称。像以下源代码这样,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法取得该函数的地址。
int (*funcptr)(int) = sfunc;int result = (*funcptr)(10);
而通过 Blocks,源代码中就能够使用匿名函数,即不带名称的函数。对于程序员而言,命名就是工作的本质,函数名、变量名、方法名、属性名、类名和框架名等都必须具备。而能够编写不带名称的函数对程序员来说相当具有吸引力。 到这里,我们知道了"带有自动变量值的匿名函数"中"匿名函数"的概念。那么"带有自动变量值"究竟是什么呢? 首先回顾一下在C语言的函数中可能使用的变量: ●自动变量(局部变量) ●函数的参数 ●静态变量(静态局部变量) ●静态全局变量 ●全局变量 其中,在函数的多次调用之间能够传递值的变量有∶ ●静态变量(静态局部变量) ●静态全局变量 ●全局变量 虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变,在任何时候以任何状态调用,使用的都是同样的变量值。
int buttonId =0;
void buttonCallback(int event) {
printf("buttonId:%d event=%d\n",buttonId,event);
}
如果只有一个按钮,那么该源代码毫无问题,可正常运行。但有多个按钮时会如何呢?
int buttonId;
void buttonCallback (int event) {
printf("buttonId:%d event=%d\n",buttonId, event);
}
void setButtonCallbacks () {
for(int i= 0;i < BUTTON_MAX;++i){
buttonId = i;
setButtonCallback(BUTTON_IDOFFSET + i,&buttonCallback);
}
}
该源代码的问题很明显。全局变量 buttonId只有一个,所有回调都使用 for循环最后的值。当然如果不使用全局变量,回调方会将按钮的ID 作为函数参数传递,就能解决该问题。
void buttonCallback(int buttonId,int event) {
printf("buttonId:%d event=8d\n",buttonId,event);
}
但是,回调方在保持回调函数的指针以外,还必须保持回调方的按钮 ID。 C++和Objective-C使用类可保持变量值且能够多次持有该变量自身。它会声明持有成员变量的类,由类生成的实例或对象保持该成员变量的值。我们来思考一下刚才例子中用来回调按钮的类。
@interface ButtonCallbackObject: NSObject {
int buttonId_;
}
@end
@implementation ButtonCallbackObject
-(id) initWithButtonId:(int)buttonId {
self =[super init];buttonId__= buttonId;
return self;
}
-(void) callback:(int)event {
NSLog(@"buttonId:%d event=8d\n",buttonId_,event);
}
@end
如果使用该类,由于对象保持按钮ID,因此回调方只需要保持对象即可。可如下使用;
void setButtonCallbacks () {
for (int i = 0;i < BUTTON MAX; ++i) {
ButtonCallbackObject *callbackObj =
[[ButtonCallbackObject alloc] initWithButtonId:i];
setButtonCallbackUsingObject(BUTTON IDOFFSET,callbackObj);
}
}
但是,由此源代码可知,声明并实现C++、Objective-C的类增加了代码的长度。 这时我们就要用到Blocks 了。Blocks 提供了类似由 C++和 Objective-C类生成实例或对象来保持变量值的方法,其代码量与编写C语言函数差不多。如"带有自动变量值",Blocks 保持自动变量的值。下面我们使用 Blocks 实现上面的按钮回调∶
void setButtonCallbacks() {
for (int i = 0; i <BUTTON_MAX; ++i) {
setButtonCallbackUsingBlock(BUTTON_IDOFFSET + i,^(int event) {
printf("buttonId:%d event=%d\n",i,event);
});
}
}
Blocks 的语法和保持自动变量值等将在后面详细说明,该源代码将"带有自动变量i值的匿名函数"设定为按钮的回调。Blocks 中将该匿名函数部分称为"Block literal",或简称为"Block"。 像这样,使用Blocks 可以不声明C++和 Obiective-C类,也没有使用静态变量、静态全局变量或全局变量时的问题,仅用编写C语言函数的源代码量即可使用带有自动变量值的匿名函数。 另外,"带有自动变量值的匿名函数"这一概念并不仅指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包(Closure)、lambda计算(λ计算,lambda calculus)等。Objective-C的 Block 在其他程序语言中的名称如下图所示:
二、Blocks模式
Block语法 下面我们详细讲解一下带有自动变量值的匿名函数 Block的语法,即 Block表达式语法(Block Literal Syntax)。前面按钮回调例子中使用的Block 语法如下∶
^(int event){
printf("buttonId:%d event=%d\n",i, event);
}
实际上,该 Block 语法使用了省略方式,其完整形式如下∶
^void (int event){
printf("buttonId:%d event=%d\n",i,event);
}
如上所示,完整形式的 Block 语法与一般的C语言函数定义相比,仅有两点不同: (1)没有函数名。 (2)带有"^“。 第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有"A”(插入记号,caret)记号。因为OS X、iOS 应用程序的源代码中将大量使用 Block,所以插入该记号便于查找。 以下为Block 语法的BN 范式。
Block literal expression ∶∶=入 block_decl compound_statement _body block decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
即使此前不了解BN 范式,通过说明也能有个概念。 "返回值类型"同C语言函数的返回值类型,"参数列表"同C语言函数的参数列表,"表达式"同C语言函数中允许使用的表达式。当然与C语言函数一样,表达式中含有return 语句时,其类型必须与返回值类型相同。 例如可以写出如下形式的 Block 语法∶
^int(int count)(return count + 1;)
虽然前面出现过省略方式,但 Block 语法可省略好几个项目。首先是返回值类型。 省略返回值类型时,如果表达式中有 return 语句就使用该返回值的类型,如果表达式中没有return 语句就使用 void类型。表达式中含有多个return 语句时,所有return的返回值类型必须相同。前面的源代码省略其返回值类型时如下所示∶
^(int count){return count + 1;}
该 Block 语法将按照 return 语句的类型,返回 int 型返回值。 其次,如果不使用参数,参数列表也可省略。以下为不使用参数的 Block 语法∶
^void (void)(printf("Blocks\n");}
该源代码可省略为如下形式∶
^{printf("Blocks\n");
返回值类型以及参数列表均被省略的 Block 语法是大家最为熟知的记述方式。如下图所示:
Block类型变量 上节中讲到的 Block语法单从其记述方式上来看,除了没有名称以及带有"入"以外,其他都与C语言函数定义相同。在定义C语言函数时,就可以将所定义函数的地址赋值给函数指针类型变量中。
int func(int count) {
return count+ 1;
}
int (*funcptr)(int) = &func;
这样一来,函数 func 的地址就能赋值给函数指针类型变量 funcptr 中了。 同样地,在 Block 语法下,可将 Block 语法赋值给声明为 Block类型的变量中。即源代码中一旦使用 Block 语法就相当于生成了可赋值给 Block类型变量的"值"。Blocks中由 Block 语法生成的值也被称为"Block"。在有关 Blocks 的文档中,"Block"既指源代码中的 Block 语法,也指由 Block 语法所生成的值。 声明 Block类型变量的示例如下∶
int (^b1k) (int);
与前面的使用函数指针的源代码对比可知,声明 Block类型变量仅仅是将声明函数指针类型变量的"*“变为"A”。该 Block类型变量与一般的C语言变量完全相同,可作为以下用途使用。 ●自动变量 ●函数参数 ●静态变量 ●静态全局变量 ●全局变量 那么,下面我们就试着使用 Block 语法将 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(int (^blk)(int)) {
在函数返回值中指定 Block 类型,可以将 Block 作为函数的返回值返回。
int (^func()(int))
return ^(int count){return count + 1;};
}
由此可知,在函数参数和返回值中使用 Block 类型变量时,记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用 typedef 来解决该问题。
typedef int (^blk t)(int);
如上所示,通过使用 typedef可声明"blk t"类型变量。我们试着在以上例子中的函数参数和函数返回值部分里使用一下。
void func (blk_t blk) {
blk_t func() {
通过使用 typedef,函数定义就变得更容易理解了。 另外,将赋值给 Block 类型变量中的 Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。变量 funcptr 为函数指针类型时,像下面这样调用函数指针类型变量∶
int result =(*funcptr)(10);
变量 blk 为 Block类型的情况下,这样调用 Block类型变量∶
int result = blk(10);
通过 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 语法和 Block 类型变量的说明,我们已经理解了"带有自动变量值的匿名函数"中的"匿名函数"。而"带有自动变量值"究竟是什么呢?“带有自动变量值"在 Blocks中表现为"截获自动变量值”。截获自动变量值的实例如下∶
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = 号d\n";
void (^b1k)(void) = ^{printf(fmt,val);};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
return 0;
}
该源代码中,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 语法瞬间的值。保存后就不能改写该值。下面我们来尝试改写截获的自动变量值,看看会出现什么结果。下面的源代码中,Block 语法之前声明的自动变量 val 的值被赋予1。
int val=0
void ("blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n",val);
以上为在 Block 语法外声明的给自动变量赋值的源代码。该源代码会产生编译错误。
error: variable is not assignable (missing __block type specifier)
void (^blk)(void)= ^{val = 1;};
若想在 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 变量。 截获的自动变量 如果将值赋值给 Block 中截获的自动变量,就会产生编译错误。
int val = 0;
void (^blk)(void) = ^{val = 1;};
该源代码会产生以下编译错误∶
error: variable is not assignable (missing __block type specifier)
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(^b1k)(void) = ^(
array =[[NSMutableArray alloc] init];
};
error: variable is not assignable(missingblock type specifier)
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语言的字符串字面量数组,而并没有向截获的自动变量赋值,因此看似没有问题。但实际上会产生以下编译错误∶
error; cannot refer to declaration with an array type inside block
printf("cAn",text[2]);note: declared here
const char text [] ="hello";
这是因为在现在的 Blocks 中,截获自动变量的方法并没有实现对C语言数组的截获。这时,使用指针可以解决该问题。
const char *text = "hello";
void (^blk)(void)= ^{
printf("%c\n",text [2]);
}
Block 的实质 Block是"带有自动变量值的匿名函数",但 Block究竟是什么呢?本节将通过Block 的实现进一步帮大家加深理解。 前几节讲的 Block 语法看上去好像很特别,但它实际上是作为极普通的C语言源代码来处理的。通过支持 Block 的编译器,含有Block 语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极为普通的C 语言源代码被编译。 这不过是概念上的问题,在实际编译时无法转换成我们能够理解的源代码,但 clang(LLVM 编译器)具有转换为我们可读源代码的功能。通过"-rewrite-objc"选项就能将含有Block 语法的源代码变换为C++的源代码。说是C++,其实也仅是使用了struct结构,其本质是C语言源代码。
clang -rewrite-objc 源代码文件名
下面,我们转换 Block 语法。
int main() {
void (^blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}
此源代码的 Block 语法最为简单,它省略了返回值类型以及参数列表。该源代码通过 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;
__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;
}
8 行的源代码竟然增加到了43行。但是如果仔细观察就能发现,这段源代码虽长却不那么复杂。下面,我们将源代码分成几个部分逐步理解。首先来看最初的源代码中的 Block 语法。
^{printf("Block\n")};
可以看到,变换后的源代码中也含有相同的表达式。
static void__main_block_func_0(struct_main_block_impl_0 *__cself){
printf("Block\n");
}
如变换后的源代码所示,通过 Blocks使用的匿名函数实际上被作为简单的C语言函数来处理。另外,根据 Block 语法所属的函数名(此处为 main)和该Block 语法在该函数出现的顺序值(此处为0)来给经 clang 变换的函数命名。
__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中花括号里面的代码内容。
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。
最后进行一个赋值操作其中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中的结构体的指针。 剩下的代码就是:初始化上面的两个成员变量
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本身。
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结构体的实例赋值给temp,然后把该实例的指针赋值给blk变量
第二行如下:
(*blk->impl.FuncPtr)(blk);
相当于源代码中的blk() 以上就是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;
fmt = "val = %d\n";
val = 10;
}
};
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;
}
和上面相比的不同之处在于下面,注意__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) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
里面出现了const char *fmt; int val; 也就是block中使用的自动变量被当作成员变量添加到了该结构体中。
该结构体的实例构造函数也发生了相应的改变:
__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中修改自动变量在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;
__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;
__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指针,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存储域 在以上例子中的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所持有,因而可以超出其变量作用域而存在。
这个非常关键,copy方法最重要的效果就是将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 函数,其中_Block_object_dispose 函数相当于release方法的函数,控制赋值给附有__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值。 Block的循环引用问题非常重要,具体详见:iOS开发“强弱共舞——weak和strong配套使用解决block循环引用问题
|