[iOS]-Blocks

一、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(int (^blk)(int) )
*/

void func (blk_t blk) {

/* 原来的记述方式
int(^func ()(int))
*/

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 可变换为以下形式∶

//经过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;
}

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; // 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指针,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会复制到堆上的情况:

  1. 调用Block的copy实例方法
  2. Block作为函数返回值返回时
  3. 在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实例方法:

  1. Block作为函数返回值返回时
  2. 将Block赋值给类的附有__strong修饰符的id类型或Block类型成员变量时
  3. 在方法名中含有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循环引用问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值