[OC学习笔记]Blocks

28 篇文章 2 订阅
24 篇文章 1 订阅

一、Blocks概要

什么是Blocks

Blocks 是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。C语言的标准不允许存在这样的函数。例如以下源代码:

int func(int count);

它声明了名称为func的函数。下面的源代码中为了调用该函数,必须使用该函数的名称 func

int result = func(10);

如果像下面这样,使用函数指针来代替直接调用函数,那么似乎不用知道函数名也能够使用该函数。

int result = (*funcptr)(10);

但其实使用函数指针也仍然需要知道函数名称。像以下源代码这样,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法取得该函数的地址。

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

而通过Blocks,源代码中就能够使用匿名函数,即不带名称的函数。对于程序员而言,命名就是工作的本质,函数名、变量名、方法名、属性名、类名和框架名等都必须具备。而能够编写不带名称的函数对程序员来说相当具有吸引力。
到这里,我们知道了“带有自动变量值的匿名函数”中“匿名函数”的概念。那么==“带有自动变量值”==究竟是什么呢?
首先回顾一下在C语言的函数中可能使用的变量。

  • 自动变量(局部变量)(不能在函数中相互传递)
  • 函数的参数(不能在函数中相互传递)
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

其中,在函数的多次调用之间能够传递值的变量有:

  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。因此虽然多次调用函数,但该变量值总能保持不变,在任何时候以任何状态调用,使用的都是同样的变量值。
声明并实现C++、Objective-C 的类增加了代码的长度。
这时我们就要用到Blocks了。Blocks 提供了类似由C++和 Objective-C 类生成实例或对象来保持变量值的方法,其代码量与编写C语言函数差不多。如“带有自动变量值”,Blocks 保持自动变量的值。
像这样,使用Blocks 可以不声明C++和Objective-C类,也没有使用静态变量、静态全局变量或全局变量时的问题,仅用编写C语言函数的源代码量即可使用带有自动变量值的匿名函数。
另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包(Closure)、lambda 计算(λ计算,lambda calculus)等。

二、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. 带有“^”。

第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号。caret)记号。因为OSX,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(^blk)(int);

与前面的使用函数指针的源代码对比可知,声明Block类型变量仅仅是将声明函数指针类变量的“*”变为“^”。该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 语法和Block 类型变量的说明,我们已经理解了“带有自动变量值的匿名函数”中的“匿名函数”。而“带有自动变量值”究竟是什么呢?“带有自动变量值”在Blocks中表为“截获自动变量值”。截获自动变量值的实例如下:

int main() {
	int dmy = 256;
	int val = 10;
	const char *fmt = "val=%d\n";
	void (^blk)(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 (missingblock 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;};

该源代码会产生编译错误。
那么截获OC对象,调用变更该对象的方法也会产生编译错误吗?

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];
};
error: variable is not assignable (missing block 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("*c\n", text[2]);

note: declared here
const char text[] = "hello";
		   ^

这是因为在现在的Blocks中,截获自动变量的方法并没有实现对C语言数组的截获。这时,使用指针可以解决该问题。

const char *text = "hello";
void (^blk)(void)= ^{
	printf("%c\n", text[2]);
};

三、Blocks的实现

(一)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 (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
	
	((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
    return 0;
}

转化后的源码很复杂,所以我们分成几个部分来理解。
先看比较像的地方:

^{
	printf("Block\n");
};

可以找到,变换后的源码也有相似的表达式:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	printf("Block\n");
}

长得好像,通过Blocks使用的匿名函数实际上被作为简单的C语言函数来处理了。另外,根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的顺序值(此处为0)来给经变换的函数命名。
该函数的参数__cself相当于C++实例方法中所指的自身变量this,或是OC实例方法中指向对象自身的变量self,即参数__cself为指向Block值的变量。

C++的this,Objective-C的self

C++中定义类的实例方法如下:

void MyClass::method(int arg)
{
printf("sp sd\n", this, arg);}

C++编译器将该方法作为C语言函数来处理。

voidZN7MyClass6methodEi(MyClass *this, int arg);
printf("sp &d\n", this, arg);

MyClass::method方法的实质就是__ZN7MyClass6methodEi函数。“this”作为第一个参数传递进去。该方法的调用如下:

MyClass cls;
cls.method(10);

该源代码通过C++编译器转换成C语言函数调用的形式:

struct MyClass cls;
ZN7MyClass6methodEi(&cls,10);

C++编译器给每个非静态的成员函数增加了一个隐藏的参数,叫 this 指针。this 指针指向当前调用对象,函数体中所有对成员变量的操作都通过该指针访问,但这些操作由编译器自动完成,不需要主动传递。即在这里this 就是MyClass类(结构体)的实例。
同样,我们也来看一下OC的实例方法:

- (void) method:(int)arg {
	NSLog(@"%p %d\n", self, arg);
}

OC编译器同C++的方法一样,也将该方法作为C语言的函数来处理。

void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg) {
	NSLog(@"*p *d\n", self,arg);
}

与C++中变换结果的this 相同,“self”作为第一个参数被传递过去。以下为调用方代码。

MyObject *obj = [[MyObject alloc] init];
[obj method:10];

如果使用clang的-rewrite-objc选项,则上面源代码会转换为:

MyObject *obj = objc msgSend(objc_getClass("MyObject"), sel_registerName("alloc"));
obj = objc _msgSend(obj, sel_registerName("init"));

objc msgSend(obj, sel_registerName("method:")10);

objc_msgSend 函数根据指定的对象和函数名,从对象持有类的结构体中检索! MyObject_method_函数的指针并调用。此时,objc_msgSend函数的第一个参数obi作为_I_MyObject_method_函数的第一个参数self 进行传递。同C++一样,self 就是 MyObject 类的对象。

遗憾的是,由这次Block语法变换而来的_main_block_func_0 函数并不使用__cself。使用参数__cself的例子将在后面介绍,我们先来看看该参数的声明。

struct __main_block_impl_0* __cself

与C++的this和Objective-C的self相同,参数__cself__main_block_impl_0 结构体的指针。
该结构体声明如下:

struct __main_block_impl_0 {
  	struct __block_impl impl;
  	struct __main_block_desc_0* Desc;
}

由于转换后的源代码中,也一并写入了其构造函数,所以看起来稍显复杂,如果除去该构造函数。__main_block_impl_0结构体会变得非常简单。第一个成员变量是impl,我们先来看一下其__block_impl结构体的声明。

struct __block_impl {
	void *isa;
	int Flags;
	int Reserved;
	void *FuncPtr;
}

代表含义:

void *isa:声明一个不确定类型的指针,用于保存Block结构体实例。
int Flags:标识符。
int Reserved:今后版本升级所需的区域大小。
void *FuncPtr:函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。

这些会在后面详细说明。第二个成员变量是Desc指针,以下为其__main_block_desc_0结构体的声明。

static struct __main_block_desc_0 {
  	size_t reserved;
  	size_t Block_size;
}

这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block的大小。
那么,下面我们来看看初始化含有这些结构体的__main_block_impl_0结构体的构造函数

__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;
}

以上就是初始化__main_block_impl_0结构体成员的源代码。我们刚刚跳过了_NSConcreteStackBlock的说明。_NSConcreteStackBlock用于初始化__block_impl结构体的isa成员。虽然大家很想了解它,但在进行讲解之前,我们先来看看该构造函数的调用。

void (*blk)(void) = (void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

因为转换较多,看起来不是很清楚,所以我们去掉转换的部分,具体如下:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

struct __main_block_impl_0 *blk = &tmp;

这样就容易理解了。该源代码将__main_block_impl0结构体类型的自动变量,即栈上生成的__main_block_impl_0 结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量 blk。以下为这部分代码对应的最初源代码。

void(^blk)(void)=^{printf("Block\n");};

将 Block 语法生成的Block赋给Block 类型变量blk。它等同于将__main_block_impl_0 结构体实例的指针赋给变量blk。该源代码中的 Block 就是__main_block_impl_0 结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例。
下面就来看看__main_block_impl_0结构体实例构造参数。

__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

第一个参数是由Block语法转换的C语言函数指针。第二个参数是作为静态全局变量初始化的__main_block_desc_0 结构体实例指针。以下为__main_block_desc_0 结构体实例的初始化部分代码。

static struct __main_block_desc_0 __main_block_desc_0_DATA = {
	0sizeof(struct __main_block_impl_0)
};

虽然大家非常迫切地想了解_NSConcreteStackBlock,不过我们还是先把其他部分讲完再对此进行说明。将__main_block_func_0 函数指针赋给成员变量 FuncPtr
我们来确认一下使用该 Block的部分。

blk();

这部分可变换为以下源代码:

((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct_block_impl *)blk);

去掉转换部分。

(*blk->impl.FuncPtr)(blk);

这就是简单地使用函数指针调用函数。正如我们刚才所确认的,由Block 语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行了传递。
到此总算摸清了 Block的实质,不过刚才没有说明的_NSConcreteStackBlock 到底是什么呢?

isa = &_NSConcreteStackBlock;

将Block指针赋给Block的结构体成员变量isa。为了理解它,首先要理解OC 类和对象的实质。其实,所谓Block 就是Objective-C 对象
“id”这一变量类型用于存储Objective-C对象。在Objective-C 源代码中,虽然可以像使用 void*类型那样随意使用id,但此id类型也能够在C语言中声明。在/usr/include/objc/runtime.h中是如下进行声明的:

typedef struct objc_object {
	Class isa;
} *id;

idobjc_object 结构体的指针类型。我们再来看看 Class。

typedef struct objc_class *Class;

Class为objc_class结构体的指针类型。objc_class结构体在/usr/include/objc/runtime.h 中声明如下:

struct objc class {
	Class isa;
};

这与objc_object结构体相同。然而,objc_object结构体和objc_class 结构体归根结底是在各个对象和类的实现中使用的最基本的结构体。下面我们通过编写简单的Objective-C类声明来认一下。

@interface MyObject : NSObject {
	int val0;
	int val1;
}
@end

基于objc_object 结构体,该类的对象的结构体如下:

struct MyObject {
	Class isa;
	int val0;
	int val1;
};

MyObject类的实例变量val0val1 被直接声明为对象的结构体成员。“Objective-C中由类生成对象”意味着,像该结构体这样“生成由该类生成的对象的结构体实例”。生成的各个对象,即由该类生成的对象的各个结构体实例,通过成员变量isa保持该类的结构体实例指针。
在这里插入图片描述
各类的结构体就是基于objc_class 结构体的class_t 结构体。class_t 结构体在 objc4 运行时库的runtime/objc-runtime-new.h 中声明如下:

struct class_t {
	struct classt *isa;
	struct class_t *superclass;
	Cache cache;
	IMP *vtable;
	uintptr_t data_NEVER_USE;
};

在 Objective-C 中,比如 NSObject 的 class_t 结构体实例以及 NSMutableArray 的class_t结构实例等,均生成并保持各个类的class_t结构体实例。该实例持有声明的成员变量、方法的名称、方法的实现(即函数指针)、属性以及父类的指针,并被Objective-C 运行时库所使用。
到这里,就可以理解Objective-C的类与对象的实质了。那么回到刚才的 Block结构体。

struct __main_block_impl_0 {
	void *isa;
	int Flags;
	int Reserved;
	void *FuncPtr;
	struct__main_block_desc_0* Desc;
}

__main_block_impl_0结构体相当于基于objc_object 结构体的Objective-C 类对象的结构体。另外,对其中的成员变量isa 进行初始化,具体如下:

isa = &_NSConcreteStackBlock;

_NConcreteStackBlock 相当于class_t 结构体实例。在将 Block 作为Objective-C 的对象处理时,关于该类的信息放置于_NSConcreteStackBlock 中。
现在大家就能理解 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;
}
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结构体内声明的成员变量类型与自动变量类型完全相同。请注意 Block 语法表达式中没有使用的自动变量不会被追加,如此源代码中的变量dmy。Blocks 的自动变量截获只针对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) {

在初始化结构体实例时,根据传递给构造函数的参数对由自动变量追加的成员变量进行初始化。以下通过构造函数调用确认其参数。

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

使用执行Block语法时的自动变量fmtval来初始化__main_block_impl_0结构体实例。即在该源代码中,__main_block_impl_0结构体实例的初始化如下:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
fmt = "val = %d\n";
val = 10;

初始化时对fmtval进行了赋值。由此可知,在__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不能直接使用C语言数组类型的自动变量。如前所述,截获自动变量时,将值传递给结构体的构造函数进行保存。
下面确认在Block 中利用C语言数组类型的变量时有可能使用到的源代码。首先来看将数组传递给Block的结构体构造函数的情况。

void func(char a[10]) {
	printf("%d\n",a[0]);
}
int main() {
	char a[10] = {2};
	func(a);
}

该源代码可以顺利编译,并正常执行。在之后的构造函数中,将参数赋给成员变量中,这样在变换了Block语法的函数内可由成员变量赋值给自动变量。源代码预测如下。

void func(char a[10]) {
	char b[10] = a;
	printf("%d\n", b[0]);
}
int main() {
	char a[10] = {2};
	func(a);
}

该源代码将C语言数组类型变量赋值给C语言数组类型变量中,这是不能编译的。虽然变量的类型以及数组的大小都相同,但C语言规范不允许这种赋值。当然,有许多方法可以截获值,但Blocks 似乎更遵循C语言规范。

(三)__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);
}

看完转换后的源代码,有没有什么发现呢?Block中所使用的被截获自动变量就如“带有自动变量值的匿名函数”所说,仅截获自动变量的值。Block中使用自动变量后,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。
以下源代码试图改变Block中的自动变量val。

int val = 0;
void(^blk)(void) = ^{val = 1;};

该代码会产生编译错误。

Variable is not assignable (missing __block type specifier)

如前所述,因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。
不过这样一来就无法在Block中保存值了,极为不便。
解决这个问题有两种方法。第一种:C语言中有一个变量,允许Block改写值。具体如下:

  • 静态变量
  • 静态全局变量
  • 全局变量

虽然Block语法的匿名函数部分简单地变换为了C语言函数,但从这个变换的函数中访问静态全局变量/全局变量并没有任何改变,可直接使用。
但是静态变量的情况下,转换后的函数原本就设置在含有Block语法的函数外,所以无法从变量作用域访问。
我们来看看下面这段源代码。

int global_val = 1;
static int static_global_val = 2;
int main()
{
	static int static_val = 3;
	void (^blk)(void) = ^{
		global_val *= 1;
		static_global_val *= 2;
		static_val *= 3;
	};
	return 0;
}

源代码使用了Block改写静态变量static_val、静态全局变量static_global_val 和全局变量 global_val。该源代码转换后如下:

int global_val = 1;
static int static_global_val = 2;
struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	int *static val;
	
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,
int *_static_val, int flags=0) : static_val(_static_val) {
		impl.isa = &_NSConcreteStackBlock;
		impl.Flags = flags;
		impl.FuncPtr = fp;
		Desc = desc;
	}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	int *static_val = __cself->static_val;
	global_val *= 1;
	static_global_val *= 2;
	(*static_val) *= 3;
}
static struct __main_block_desc_0 {
	unsigned long reserved;
	unsigned long Block_size;
} __main_block_desc_0_DATA = {
	0sizeof(struct__main_block_impl_0)
};
int main() {
	static int static val = 3;
	blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val); 
	return 0;
}

这个结果是大家很熟悉的,对静态全局变量 static_global_val 和全局变量 global_val 的访问与转换前完全相同。静态变量static_val 又要如何转换的呢?以下摘出Block 中使用该变量的部分。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	int *static_val = __cself->static_val;
	
	(*static_val) *= 3;
}

使用静态变量static_val的指针对其进行访问。将静态变量static_val的指针传递给main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量的最简单方法。
静态变量的这种方法似乎也适用于自动变量的访问。但是我们为什么没有这么做呢?
实际上,在由Block语法生成的值Block上,可以存有超过其变量作用域的被截获对象的自动变量。变量作用域结束的同时,原来的自动变量被废弃,因此 Block 中超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。这些在下节详细说明。
解决Block中不能保存值这一问题的第二种方法是使用“__block说明符”。更准确的表述方式为“__block存储域类说明符”(__block storage-class-specifier)。C语言中有以下存储域说明符:

  • typedef
  • extern
  • static
  • auto
  • register

__block 说明符类似于 static、auto 和 register 说明符,它们用于指定将变量值设置到哪个存储域中。例如,auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。
下面我们来实际使用__block说明符,用它来指定Block中想变更值的自动变量。我们在前面编译错误的源代码的自动变量声明上追加__block 说明符。

__block int val = 10;
void (^blk)(void) = ^{val = 1;};

该源代码可进行编译。变换后如下:

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_byrefval_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;
}
static void_main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_imp1_0*src) {
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
	unsigned long reserved;
	unsigned long 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(structmain_block_impl_0),
	__main_block_copy_O,
	__main_block_dispose_0
};
int main() {
	__Block_byref_val_0 val = {
		0,
		&val,
		0,
		sizeof(__Block_byref_val_0),
		10
	};
	blk = &__mainblock_impl_0(
__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);

	return 0;
}

只是在自动变量上附加了__block说明符,源代码量就急剧增加。

__block int val = 10;

这个__block变量val是怎样转换过来的呢?

__Block_byref_val_0 val = {
	0,
	&val,
	0,
	sizeof(_Block_byref_val_0),
	10
};

我们发现,它竟然变为了结构体实例__block变量也同 Block一样变成__Block_byref_val_0结构体类型的自动变量,即栈上生成的__Block_byref_val_0 结构体实例。该变量初始化为10,且这个值也出现在结构体实例的初始化中,这意味着该结构体持有相当于原自动变量的成员变量。
该结构体声明如下:

struct __Block_byref_val_0 {
	void *__isa;
	__Block_byref_val_0 *_forwarding;
	int __flags;
	int __size;
	int val;
};

如同初始化时的源代码,该结构体中最后的成员变量val是相当于原自动变量的成员变量,我们从它的名称也能看出来这一点。
下面这段给_block变量赋值的代码又如何呢?

^{val = 1;}

该源代码转换如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	__Block_byref_val_0 *val =__cself->val;
	
	(val->__forwarding->val) = 1;
}

刚在Block 中向静态变量赋值时,使用了指向该静态变量的指针。而向__block变量赋值要比这个更为复杂。Block 的__main_block_impl_0 结构体实例持有指向__block 变量的__Block_ byref_val_0 结构体实例的指针。
__Block_byref_val_0 结构体实例的成员变量__forwarding 持有指向该实例自身的指针。通过成员变量__forwarding 访问成员变量val。(成员变量 val 是该实例自身持有的变量,它相当于原自动变量。)

在这里插入图片描述

究竟为什么会有成员变量__forwarding呢?这个问题,我们留到下节详细说明。
另外,__block变量的__Block_byref_val_0 结构体并不在 Block 用__main_block_impl_0 结构体中,这样做是为了在多个Block 中使用__block变量。我们看一下下面的源代码。

__block int val = 10;
void (^blk0)(void) = ^{val = 0;};
void (^blk1)(void) = ^{val = 1;};

Block类型变量blk0blk1访问__block变量val。我们把这两部分源代码的转换结果摘录出来。

__Block_byref_val_0 val = {0, &val, 0, sizeof(_Block_byref_val_0), 10};

blk0 = &__main_block_impl_0(__main_block_func_0, &_main_block_desc_0_DATA, &val, 0x22000000);

blkl = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 0x22000000);

两个Block 都使用了__Block_byref_val_0结构体实例val的指针。这样一来就可以从多个 Block 中使用同一个__block 变量。当然,反过来从一个 Block中使用多个__block 变量也是可以的。只要增加 Block 的结构体成员变量与构造函数的参数,便可对应使用多个__block 变量。
到此大概能够理解__block变量了。下节主要说明之前跳过部分的内容:

  • Block超出变量作用域可存在的理由
  • __block变量的结构体成员变量__forwarding存在的理由
    另外,将在后面中说明__main_block_desc_0结构体中增加的成员变量copydispose

(四)Block存储域

通过前面说明可知,Block 转换为 Block 的结构体类型的自动变量,__block 变量转换为 block 变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如表2-2所示。

名称实质
Block栈上Block的结构体实例
__block 变量栈上__block变量的结构体实例

另外,通过之前的说明可知 Block也是Objective-C 对象。将Block当作Objective-C对象来看时,该Block的类为_NSConcreteStackBlock。虽然该类并没有出现在已变换源代码中,但有很多与之类似的类,如:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

首先,我们能够注意到_NSConcreteStackBlock 类的名称中含有“栈”(stack)一词,即该类的对象Block设置在栈上。
同样地,_NSConcreteGlobalBlock类对象如其名“全局”(global)所示,与全局变量一样,设置在程序的数据区域(.data区)中。
NSConcreteMallocBlock类对象则设置在由malloc 函数分配的内存块(即堆)中。具体整理如表。

设置对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock程序的数据区域(data区)
_NSConcreteMallocBlock

在这里插入图片描述
到现在为止出现的Block例子使用的都是_NSConcreteStackBlock 类,且都设置在栈上。但实际上并非全是这样,在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象,如:

void (^blk)(void) = ^{printf("Global Block\n");}
int main() {

此源代码通过声明全局变量blk来使用Block 语法。如果转换该源代码,就会生成在3.1中讲到的那种Block,Block用结构体的成员变量isa的初始化如下:

impl.isa = &_NSConcreteGlobalBlock;

该 Block 的类为_NSConcreteGlobalBlock 类。此 Block 即该 Block 用结构体实例设置在程序的数据区域中。因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block用结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例。因此将Block用结构体实例设置在与全局变量相同的数据区域中即可。
只在截获自动变量时,Block 用结构体实例截获的值才会根据执行时的状态变化。例如以下源代码中,虽然多次使用同一个Block语法,但每个for循环中截获的自动变量的值都不同。

typedef int(^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
	blk_t blk = ^(int count) {return rate * count;};
}

上面 Block 语法生成的 Block 用结构体实例在每次 for 循环中截获的值都不同。但是以下源代码中在不截获自动变量时,Block用结构体实例每次截获的值都完全相同。

typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) {
	blk_t blk = ^(int count){return count;};
}

也就是说,即使在函数内而不在记述广域变量的地方使用Block语法时,只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。
虽然通过clang转换的源代码通常是_NSConcreteStackBlock类对象,但实现上却有不同。总结如下:

  • 记述全局变量的地方有Block语法时
  • Block语法的表达式中不使用应截获的自动变量时

在以上这些情况下,Block 为_NSConcreteGlobalBlock 类对象。即Block配置在程序的数据域中。除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
那么将 Block配置在堆上的_NSConcreteMallocBlock类在何时使用呢?这正是上一节最后遗留问题的答案。上节遗留问题为:

  • Block 超出变量作用域可存在的原因。
  • __block变量用结构体成员变量__forwarding 存在的原因

配置在全局变量上的Block,从变量作用域外也可以通过指针安全地使用。但设置在栈上的 Block,如果其所属的变量作用域结束,该 Block 就被废弃。由于__block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该__block 变量也会被废弃。
在这里插入图片描述
变量作用域结束后,栈上的__block变量和Block也被废弃。
Blocks 提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。
在这里插入图片描述
复制到堆上的Block将_NSConcreteMallocBlock 类对象写入 Block 用结构体实例的成员变量isa

impl.isa = &_NSConcreteMallocBlock;

__block 变量用结构体成员变量__forwarding 可以实现无论__block变量配置在栈上还是堆上时都能够正确地访问__block变量。
有时在_block变量配置在堆上的状态下,也可以访问栈上的__block 变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block 变量还是从堆上的__block 变量都能够正确访问
那么Blocks 提供的复制方法究竟是什么呢?实际上当ARC有效时,大多数情形下编译器会恰当地进行判断,自动生成将Block从栈上复制到堆上的代码。我们来看一下下面这个返回 Block的函数。

typedef int (^blk_t)(int);

blk_t func(int rate) {
	return ^(int count){return rate * count;};
}

该源代码为返回配置在栈上的 Block 的函数。即程序执行中从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。虽然有这样的问题,但该源代码通过对应ARC 的编译器可转换如下:

blk_t func(int rate) {
blk_t tmp = &__func_block_impl_0(__func_block_func_0,&__func_block_desc_0_DATA, rate);

tmp = objc_retainBlock(tmp);

return objc_autoreleaseReturnValue(tmp);

另外,因为ARC处于有效的状态,所以blk_t tmp实际上与附有__strong 修饰符的blk_t __strong tmp 相同。
然而通过 objc4运行时库的runtime/objc-arrmm可知,objc_retainBlock函数实际上就是_Block_copy 函数。即:

tmp = _Block_copy(tmp);

return objc_autoreleaseReturnValue(tmp);

过程中到底发生了什么呢?我们通过下列源代码中的注释来看看。

/*
 *将通过Block语法生成的Block,
 *即配置在栈上的Block用结构体实例
 *赋值给相当于Block类型的变量tmp中。
 */
tmp=_Block_copy(tmp);
/*
 *_Block_copy 函数
 *将栈上的Block复制到堆上。
 *复制后,将堆上的地址作为指针赋值给变量tmp。
 */

return objc_autoreleaseReturnValue(tmp);
/*
 *将堆上的Block作为Objective-c对象
 *注册到autoreleasepool中,然后返回该对象。
 */

将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。
前面讲到过“大多数情况下编译器会适当地进行判断”,不过在此之外的情况下需要手动生成代码,将 Block 从栈上复制到堆上。此时我们使用“copy实例方法”。这就是ARC 一章中大量出现的alloc/new/copy/mutableCopy方法中的一个方法,copy方法。那么编译器不能进行判断究竟是什么样的状况呢?如下所示:

  • 向方法或函数的参数中传递Block时

是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下方法或函数不用手动复制。

  • Cocoa 框架的方法且方法名中含有usingBlock 等时
  • Grand Central Dispatch 的 API

举个具体例子,在使用NSArray类的enumerateObjectsUsingBlock 实例方法以及dispatch_async 函数时,不用手动复制。相反地,在 NSArray 类的 initWithObjects 实例方法上传递 Block时需要手动复制。下面我们来看看源代码。

- (id)getBlockArray {
	int val =10;
	
	return [[NSArray alloc] initWithObjects:
		^{NSLog(@"blk0:%d",val);},
		^{NSLog(@"blk1:%d",val);}, nil];
}

getBlockArray方法在栈上生成两个Block,并传递给NSArray类的initWithObjects 实例方法。下面,在getBlockArray 方法调用方,从 NSArray 对象中取出 Block 并执行。

id obj = getBlockArray();

typedef void (^blk_t)(void);

blk_t blk =(blk_t)[obj objectAtIndex:0];

blk();

该源代码的blk(),即Block在执行时发生异常,应用程序强制结束。这是由于在 getBlockArray 函数执行结束时,栈上的Block 被废弃的缘故。可惜此时编译器不能判断是否需要复制。也可以不让编译器进行判断,而使其在所有情况下都能复制。但将Block 从栈上复制到堆上是相当消耗CPU的。当Block设置在栈上也能够使用时,将Block从栈上复制到堆上只是在浪费CPU资源。因此只在此情形下让编程人员手动进行复制。
该源代码像下面这样修改一下即可正常运行。

- (id)getBlockArray {
	int val = 10;
	
	return [[NSArray alloc] initWithObjects:
		[^{NSLog(@"blk:%d", val);} copy],
		[^{NSLog(@"blk1:%d",val);} copy], nil];
}

虽然看起来有点奇怪,但像这样,对于Block 语法可直接调用copy方法。当然对于Block类型变量也可以调用copy方法。

typedef int (^blk_t)(int);

blk_t blk = ^(int count){return rate * count;};

blk = [blk copy];

另外,对于已配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy方法又会如何呢?下面按配置Block的存储域,将copy方法进行复制的动作总结了出来。

Block 的类副本源的配置存储域复制效果
NSConcreteStackBlockc栈从栈复制到堆
NSConcreteGlobalBlock程序的数据区域什么也不做
NSConcreteMallocBlock引用计数增加

不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。但是在ARC中不能显式地release,那么多次调用copy方法进行复制有没有问题呢?我们看一下下面代码。

blk = [[[[blk copy] copy] copy] copy];

该源代码可解释如下:

{
	blk_t tmp = [blk copy];
	blk = tmp;
}
{
	blk_t tmp = [blk copy];
	blk = tmp;
}
{
	blk_t tmp = [blk copy];
	blk = tmp;
}
{
	blk_t tmp = [blk copy];
	blk = tmp;
}

加入注释:

{
	/*
	 *将配置在栈上的Block*赋值给变量blk中。
	 */
	blk_t tmp = [blk copy];
	/*
	 *将配置在堆上的Block赋值给变量tmp中,
	 *变量 tmp持有强引用的Block。
	 */
	blk = tmp;
	/*
	 *将变量tmp的Block赋值为变量blk,变量blk 持有强引用的Block。
 	 *因为原先赋值的Block配置在栈上,所以不受此赋值的影响。
	 *此时Block的持有者为*变量blk和变量tmp。
	 */
}	/*
	 *由于变量作用域结束,所以变量tmp被废弃,
	 *其强引用失效并释放所持有的Block。
	 *由于Block被变量blk持有,
	 *所以没有被废弃。
	 */
{
	/*
	 *配置在堆上的 Block 被赋值变量blk,
	 *同时变量blk*持有强制引用的Blocko
	 */
	blk_t tmp = [blk copy];
	/*
	 *配置在堆上的Block 被赋值到变量tmp中,变量tmp
	 *持有强引用的 Block。
	 */
	blk = tmp;
	/*
	 *由于向变量blk进行了赋值,
 	 *所以现在赋值的 Block的强引用失效,
	 *Block被释放。
	 *由于Block被变量tmp所持有,所以没有被废弃。
	 *变量blk中赋值了变量tmp的Block,变量blk持有强引用的Block。
	 *此时Block的持有者为变量blk和变量tmp。
	 */
}	/*	
	 *由于变量作用域结束,变量tmp则被废弃,
	 *其强引用失效并释放所持有的Block。
	 *由于变量blk还处于持有的状态,
	 *Block 没有被废弃。
	 */

/*
 *下面重复此过程
 */

由此可看出,ARC 有效时完全没有问题。

(五)__block变量存储域

上节只对 Block 进行了说明,那么对__block变量又是如何处理的呢?使用__block变量的 Block 从栈复制到堆上时,__block 变量也会受到影响。总结如表所示。

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 持有__block 变量,并增加__block变量的引用计数。

在这里插入图片描述
如果配置在堆上的Block被废弃,那么它所使用的 __block变量也就被释放。
在这里插入图片描述
在这里插入图片描述
到这里我们可以看出,此思考方式与OC 的引用计数式内存管理完全相同。使用 block变量的 Block 持有__block 变量。如果 Block 被废弃,它所持有的__block变量也就被释放
那么在理解了__block变量的存储域之后,我们再回顾一下 讲使用__block 变量用结构体成员变量__forwarding 的原因。“不管__block 变量配置在栈上还是在堆上,都能够正确地访问该变量”。正如这句话所述,通过 Block 的复制,__block 变量也从栈复制到堆。此时可同时访问栈上的__block变量和堆上的__block 变量。源代码如下:

__block int val = 0;

void (^blk)(void) = [^{++val;} copy];

++val;

blk();

NSLog(@"%d",val);

利用copy方法复制使用了__block变量的Block语法。Block和__block变量两者均是从栈复制到堆。此代码中在Block语法的表达式中使用初始化后的__block变量。

^{++val;}

然后在 Block 语法之后使用与Block 无关的变量。

++val;

以上两种源代码均可转换为如下形式:

++(val.__forwarding->val);

在变换Block语法的函数中,该变量val为复制到堆上的__block变量用结构体实例,而使用的与 Block 无关的变量val,为复制前栈上的__block变量用结构体实例。
但是栈上的__block 变量用结构体实例在__block 变量从栈复制到堆上时,会将成员变量 __forwarding 的值替换为复制目标堆上的__block变量用结构体实例的地址。
在这里插入图片描述
通过该功能,无论是在 Block 语法中、Block 语法外使用__block变量,还是__block 变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。

(六)截获对象

1. 课本知识

以下源代码生成并持有NSMutableArray类的对象,由于附有__strong 修饰符的赋值目标变量的作用域立即结束,因此对象被立即释放并废弃。

{
	id array = [[NSMutableArray alloc] init];
}

我们来看一下在Block语法中使用该变量array的代码:

blk_t blk;
{
	id array = [[NSMutableArray alloc] init];
	blk = [^(id obj) {
		[array addObject:obj];
		NSLog(@"array count = %ld", [array count]);
	} copy];
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]); 
blk([[NSObject alloc] init]);

变量作用域结束的同时,变量array 被废弃,其强引用失效,因此赋值给变量array的 NSMutableArray 类的对象必定被释放并废弃。但是该源代码运行正常,其执行结果如下:

array count = 1
array count = 2
array count = 3

这一结果意味着赋值给变量array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在。通过编译器转换后的源代码如下:

/*Block用结构体/函数部分*/
struct __main_block_impl_0 {
	struct __block_impl_impl;
	struct __main_block_desc_0* Desc;
	id __strong array;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0) : array(_array) {
		impl.isa = &_NSConcreteStackBlock;
		impl.Flags = flags;
		impl.FuncPtr = fp;
		Desc = desc;
	}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
	id strong array = __cself->array;
	[array addObject:obj];
	NSLog(@"array count = %ld", [array count]);
}

static void __main_block_copy_0(struct __main_block_impl_o *dst, struct __main_block_impl_0 *src) {
	_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

static void_main_block_dispose_0(struct __main_block_impl_0 *src) {
	_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

static struct __main block desc_0 {
	unsigned long reserved;
	unsigned long 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
};
/*Block语法,使用Block部分 */
blk_t blk;
{
	id __strong array = [[NSMutableArray alloc] init];
	blk = &_main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
	blk = [blk copy];
}

(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*b1k->impl.FuncPtr)(blk, [[NSObject alloc] init]);

请注意被截获的自动变量array。我们可以发现它是Block用的结构体中附有__strong 修饰符的成员变量。

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	id __strong array;	
};

在Objective-C中,C语言结构体不能含有附有strong修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。
但是Obiective-C的运行时库能够准确把握Block从栈复制到堆以及堆上的Block 被废弃的时机,因此Block 用结构体中即使含有附有__strong修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copydispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和_main_block_dispose_0函数。
由于在该源代码的Block用结构体中,含有附有__strong修饰符的对象类型变量array,所以需要恰当管理赋值给变量array的对象。因此__main_block_copy_0 函数使用_Block_object_assign 函数将对象类型对象赋值给Block用结构体的成员变量array中并持有该对象。

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
	_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

_Block_object_assign 函数调用相当于retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。
另外,__main_block_dispose_0 函数使用_Block_object_dispose 函数,释放赋值在 Block 用结构体成员变量array中的对象。

static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
	_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}

_Block_object_dispose 函数调用相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。
虽然此__main_block_copy_0 函数(以下简称 copy 函数)和__main_block_dispose_0 函数(以下简称 dispose 函数)指针被赋值在__main_block_desc_0 结构体成员变量copydispose 中,但在转换后的源代码中,这些函数包括使用指针全都没有被调用。那么这些函数是从哪调用呢?
在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。
调用copy函数和dispose 函数的时机:

函数 | 调用时机

  • | -
    copy函数 | 栈上的 Block 复制到堆时
    dispose函数 | 堆上的 Block 被废弃时

那么什么时候栈上的 Block 会复制到堆呢?

  • 调用Block的copy实例方法时 Block作为函数返回值返回时
  • 将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 在方法名中含有usingBlock的 Cocoa 框架方法或 Grand Central Dispatch 的 API 中传递 Block 时

在调用Block的 copy实例方法时,如果Block配置在栈上,那么该Block 会从栈复制到堆。Block作为函数返回值返回时、将Block赋值给附有__strong 修饰符id类型的类或Block类型员变量时,编译器自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同。在方法名中含有usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的API 中传递 Block 时,在该方法或函数内部对传递过来的Block 调用 Block 的 copy 实例方法或者_Block_copy函数。
也就是说,虽然从源代码来看,在上面这些情况下栈上的Block 被复制到堆上,但其实可归结为_Block_copy函数被调用时 Block 从栈复制到堆。
相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的 dealloc 实例方法。
有了这种构造,通过使用附有__strong修饰符的自动变量,Block 中截获的对象就能够超出其变量作用域而存在。
虽然这种使用copy函数和dispose函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了。

static void __main_block_copy0(
	struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

转换后的源代码在Block用结构体的部分基本相同,其不同之处:
截获对象时和使用__block变量时的不同

对象__block 变量
BLOCK_FIELDJS_OBJECTBLOCK_FIELD_IS_BYREF

通过BLOCK_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF 参数,区分copy函数和 dispose 函数的对象类型是对象还是__block变量。
但是与copy函数持有截获的对象、dispose函数释放截获的对象相同,copy函数持有所使用的block变量,dispose函数释放所使用的__block变量。
由此可知,Block中使用的赋值给附有__strong修饰符的自动变量的对象和复制到堆上的__block 变量由于被堆上的Block所持有,因而可超出其变量作用域而存在。
那么在刚才的源代码中,如果不调用Block的copy实例方法又会如何呢?

blk_t blk;
{
	id array =[[NSMutableArray alloc] init];
	blk = ^(id obj) {
		[array addObject:obj];
		NSLog(@"array count = %ld",[array count]);
	};
}

blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

执行该源代码后,程序会强制结束。
因为只有调用_Block_copy函数才能持有截获的附有__strong 修饰符的对象类型的自动变量值,所以像上面源代码这样不调用_Block_copy函数的情况下,即使截获了对象,它也会随着变量作用域的结束而被废弃。
因此,Block中使用对象类型自动变量时,除以下情形外,推荐调Block的copy实例方法。

  • Block作为函数返回值返回时
  • 将Block 赋值给类的附有__strong修饰符的id类型或Block类型成员变量时。向方法名中含有usingBlock的Cocoa 框架方法或Grand Central Dispatch 的API中传递 Block 时

2. 奇怪的地方

本以为对此的研究可以到此为止了,但是傻孩子🤪在自己电脑上跑了一下:

#import <Foundation/Foundation.h>

typedef void (^blk_t)(id);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        blk_t blk;
        {
            id array = [[NSMutableArray alloc] init];
            blk = ^(id obj) {
                [array addObject:obj];
                NSLog(@"array count = %ld", [array count]);
            };
        }

        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
        blk([[NSObject alloc] init]);
    }
    return 0;
}

程序输出了:
在这里插入图片描述
好像没什么问题啊。加个copy肯定也是没问题的。
好奇怪,前面说过:将Block 赋值给附有__strong修饰符id类型的类或Block类型成员变量时,栈上的 Block 会复制到堆。是不是这个原因呢?
看看编译器转换后的.cpp看看:

typedef void (*blk_t)(id);

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __strong id array;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __strong id _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, __strong id obj) {
    __strong id array = __cself->array; // bound by copy

                ((void (*)(id, SEL, ObjectType  _Nonnull __strong))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
                NSLog((NSString *)&__NSConstantStringImpl__var_folders_rx_h53wjns9787gpxxz8tg94y6r0000gn_T_main_22bef0_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
            }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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[]) {
    /* @autoreleasepool */ {
        __AtAutoreleasePool __autoreleasepool;
        blk_t blk;
        {
            id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
            blk = ((void (*)(__strong id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
        }

        ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
        ((void (*)(__block_impl *, __strong id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
    }
    return 0;
}

拿到了这个转换后的代码,看一下嗷,main的部分内容:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        __AtAutoreleasePool __autoreleasepool;
        blk_t blk;
        {
            id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
            blk = ((void (*)(__strong id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344));
        }
    }
    return 0;
}

再看看加了copy的代码转换后的情况:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        blk_t blk;
        {
            id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));

            blk = (blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(__strong id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));

        }
    }

    return 0;
}

可以看到,赋值的地方多了这么个东东:

sel_registerName("copy")

和书上讲述的相同,不手动添加copy是不会有这个东西的。接下来我们看一下引用计数:

blk_t blk;
{
	id array = [[NSMutableArray alloc] init];
	NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
	blk = ^(id obj) {
		[array addObject:obj];
		NSLog(@"array count = %ld", [array count]);
	};
	NSLog(@"count:%ld", CFGetRetainCount((__bridge CFMutableArrayRef)array));
}

结果:
在这里插入图片描述
可以看到,引用计数变成了3,并不是2。
添加copy,运行结果相同。
声明成__unsafe_unretained康康:

blk_t __unsafe_unretained blk;

在这里插入图片描述
计数少了1。这就是只保留了栈上的Block。通过__unsafe_unretained进行修饰,即可只保留栈空间的Block。在ARC中,指向OC对象的变量没有明确写所有权修饰符,则修饰符默认为__strong(前面学到了,实际上Block就是一种OC对象)。
此外,我们可以再进行研究,重写自定义类的dealloc方法看看:

在这里插入图片描述

不复制到栈上,在代码块后面打断点,断点前输出为:
在这里插入图片描述

说明超出作用域的对象被废弃了。
在这里插入图片描述
使用了块对其截获,没有输出,说明其没有被废弃,已经被复制到堆上了。
在这里插入图片描述
使用__unsafe_unretained,又输出:
在这里插入图片描述

3. 四种截获对象场景

(1)ARC环境+堆上的Block+强指针Person *__strong person

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        blk_t2 blk2;
        {
            Person *person = [[Person alloc] init];
            person.age = 1;
            blk2 = ^{
                NSLog(@"person age = %d", person.age);
            };
        }
        
        NSLog(@"end");
        blk2();
    }
    return 0;
}

将上述代码转换:

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	Person *__strong person;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
	}
};

可以看到,这种情况下捕获的类型是:

Person *__strong person;

(2)ARC环境+堆上的block+弱指针__weak Person *person
在这里插入图片描述
断点处打印:
在这里插入图片描述
weak_personperson弱引用,临时作用域失效,person释放,弱引用失效。完整打印:
在这里插入图片描述
查看转换后的.cpp代码:

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	Person *__weak weak_person;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weak_person, int flags=0) : weak_person(_weak_person) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

截获的变量为:

Person *__weak weak_person;

(3)ARC环境+栈上的Block+强指针Person *__strong person
在这里插入图片描述
断点处输出:
在这里插入图片描述
结果显示栈上的block使用强指针Person *person,没有影响person所指向对象的生命周期,出了临时作用域的之后就被释放了。
转换后代码:

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	Person *__strong person;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

(4)ARC环境+栈上的block+弱指针__weak Person *person
在这里插入图片描述
输出为:
在这里插入图片描述
转换后:

struct __main_block_impl_0 {
	struct __block_impl impl;
	struct __main_block_desc_0* Desc;
	Person *__weak weak_person;
	__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weak_person, int flags=0) : weak_person(_weak_person) {
    	impl.isa = &_NSConcreteStackBlock;
    	impl.Flags = flags;
    	impl.FuncPtr = fp;
    	Desc = desc;
  	}
};

截获的变量为:

Person *__weak weak_person;

从上面几个“实验🧪”可以看出来,只有堆上的Block使用强指针的时候,才会影响该指针所指对象的生命周期。也就是书上所说的第一种情况。多调用了一次“copy”把Block从栈上拷贝到堆上。呼,好难😯。

(七)__block变量和对象

__block 说明符可指定任何类型的自动变量。下面指定用于赋值 Objective-C 对象的 id 类型自动变量。

__block id obj = [[NSObject alloc] init];

其实该代码等同于:

__block id __strong obj = [[NSObject alloc] init];

ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。该代码可通过clang 转换如下:

/*_block变量用结构体部分*/
struct __Block byref_obj_0 {
	void *__isa;
	_Block_byref_obj_0 *_forwarding;
	int __flags;
	int __size;
	void (*__Block_byref_id_object_copy)(void*,void*);
	void (*__Block_byref_id_object_dispose)(void*);
	__strong id obj;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
	_Block_object_assign((char*)dst + 40, *(void * *)((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *sre) {
	_Block_object_dispose(*(void * *)((char*)src + 40), 131);
}
/*_block变量明部分*/
__Block_byref_obj_0 obj = {
	0,
	&obj,
	0x2000000,
	sizeof(__Block_byref_obj_0),
	__Block_byref_id_object_copy_131,
	__Block_byref_id_object_dispose_131,
	[[NSObject alloc] init]
};

在这里出现了前面讲到的_Block_object_assign 函数和_Block_object_dispose 函数。
在 Block 中使用附有__strong 修饰符的 id 类型或对象类型自动变量的情况下,当Block 从栈复制到堆时,使用_Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_Block_object_dispose 函数,释放Block 截获的对象。
在block变量为附有__strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block 变量从栈复制到堆时,使用_Block_object_assign 函数,持有赋值给__block变量的对象。当堆上的__block 变量被废弃时,使用_Block_object_dispose 函数,释放赋值给 Block 变量的对象。
由此可知,即使对象赋值复制到堆上的附有__strong修饰符的对象类型__block 变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block 中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。
另外,我们前面用到的只有附有__strong 修饰符的id类型或对象类型自动变量。如果使用__weak 修饰符会如何呢?首先是在Block 中使用附有__weak 修饰符的 id 类型变量的情况。

blk_t blk;
{
	id array =[[NSMutableArray alloc] init];
	id __weak array2 = array;
	
	blk = [^(id obj) {
		[array2 addobject:obj];
		NSLog(@"array2 count = %ld", [array2 count]);
	} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

该代码执行结果与前面不同。

array2 count = 0
array2 count = 0
array2 count = 0

这是由于附有__strong修饰符的变量array 在该变量作用域结束的同时被释放、废弃,nil 被赋值在附有__weak修饰符的变量array2 中。该代码可正常执行,具体如下。
若同时指定__block说明符和__weak 修饰符会怎样呢?

blk_t blk;
{
	id array =[[NSMutableArray alloc] init];
	__block id __weak array2 = array;
	
	blk = [^(id obj) {
		[array2 addobject:obj];
		NSLog(@"array2 count = %ld", [array2 count]);
	} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

该代码执行结果与前面相同。

array2 count = 0
array2 count = 0
array2 count = 0

这是因为即使附加了__block说明符,附有__strong修饰符的变量array也会在该变量作用域结束的同时被释放废弃,nil 被赋值给附有__weak 修饰符的变量 array2
另外,由于附有__unsafe_unretained 修饰符的变量只不过与指针相同,所以不管是在Block中使用还是附加到__block变量中,也不会像__strong修饰符或__weak修饰符那样进行处理。因此在使用附有__unsafe_unretained 修饰符的变量时,注意不要通过悬垂指针访问已被废弃的对象。
因为并没有设定__autoreleasing 修饰符与Block同时使用的方法,所以没必要使用 autoreleasing 修饰符。另外,它与__block 说明符同时使用时会产生编译错误

__block id __autoreleasing obj = [[NSObject alloc] init];

变量obj同时指定了__autoreleasing 修饰符和__block 说明符,这会引起编译错误:

error: block variables cannot have autoreleasing ownership

(八)Block循环引用

口果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block 从栈复制到堆时,该对象为Block 所持有。这样容易引起循环引用。我们来看看下面的源代码:

typedef void (^blk_t)(void);
@interface MyObject : NSObject
{
	blk_t blk_;
}
@end

@implementation MyObject

- (id)init {
	self = [super init];
	blk_ = ^{
		NSLog(@"self = %@", self);
	};
	return self;
}
- (void)dealloc {
	NSLog(@"dealloc");
}
@end

int main(){
	id o = [[MyObject alloc] init];
	NSLog(@"%@", o);
	return 0;
}

该源代码中MyObject 类的 dealloc 实例方法一定没有被调用。
MyObject 类对象的 Block 类型成员变量blk_持有赋值为Block 的强引用。即 MyObject 类对象持有Block。init 实例方法中执行的 Block 语法使用附有__strong 修饰符的 id 类型变量 self。并且由于Block 语法赋值在了成员变量blk_中,因此通过Block 语法生成在栈上的 Block 此时由栈复制到堆,并持有所使用的selfself持有Block,Block 持有 self。这正是循环引用。

在这里插入图片描述

编译器在编译该源代码时能够查出循环引用,因此编译器能正确地进行警告。

Capturing 'self' strongly in this block is likely to lead to a retain cycle

为避免此循环引用,可声明附有__weak 修饰符的变量,并将 self 赋值使用。

-(id)init
{
	self = [super init];
	id __weak tmp = self;
	blk_ = ^{
		NSLog(@"self = %@",tmp);
	};
	return self;
}

在这里插入图片描述
在该源代码中,由于Block存在时,持有该Block 的 MyObject 类对象即赋值在变量tmp中的 self 必定存在,因此不需要判断变量 tmp 的值是否为nil
在面向iOS4,Snow Leopard 的应用程序中,必须使用__unsafe_unretained 修饰符代替__weak 修饰符。在此源代码中也可使用__unsafe_unretained 修饰符,且不必担心悬垂指针。

- (id)init
{
	self = [super init];
	id __unsafe_unretained tmp = self;
	blk_ = ^{
		NSLog(@"self = %@",tmp);
	};
	return self;
}
@interface MyObject : NSObject
{
    blk_t blk_;
    id obj_;
}
@end

@implementation MyObject

- (id)init {
    self = [super init];
    blk_ = ^{
        NSLog(@"obj_ = %@", obj_);
    };
    return self;
}

编译器继续进行警告。

Capturing 'self' strongly in this block is likely to lead to a retain cycle

即Block语法内使用的 obj_实际上截获了self。对编译器来说,obj_只不过是对象用结构体的成员变量。

blk_ = ^{
	NSLog(@"obj_ = %@", self->obj_);
};

该源代码也基本与前面一样,声明附有__weak修饰符的变量并赋值obj_使用来避免循环引用。在此源代码中也可安全地使用__unsafe_unretained 修饰符,原因同上。

- (id)init {
    self = [super init];
    id __weak obj = obj_;
    blk_ = ^{
        NSLog(@"obj_ = %@", obj);
    };
    return self;
}

在为避免循环引用而使用__weak修饰符时,虽说可以确认使用附有__weak 修饰符的变量时是否为nil,但更有必要使之生存以使用赋值给附有__weak修饰符变量的对象。
另外,还可以使用__block变量来避免循环引用

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
	blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
	self = [super init];
	
	__block id tmp = self;
	blk_ = ^{
		NSLog(@"self = %@", tmp);
		tmp = nil;
	};
	return self;
}
- (void)execBlock {
	blk_();
}
-(void)dealloc {
	NSLog(@"dealloc");
}
@end

int main() {
	id o = [[MyObject alloc] init];
	[o execBlock];
	return 0;
}

该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk_的 Block,便会循环引用并引起内存泄漏。在生成并持有MyObject 类对象的状态下会引起以下循环引用。

  • MyObject 类对象持有Block
  • Block 持有__block变量
  • __block变量持有MyObject类对象

在这里插入图片描述
如果不执行 execBlock 实例方法,就会持续该循环引用从而造成内存泄漏。
通过执行 execBlock实例方法,Block 被实行,nil 被赋值在__block变量tmp中。

 blk_ = ^{
	NSLog(@"self = %@", tmp);
	tmp = nil;
};

因此,__block 变量 tmp 对 MyObject 类对象的强引用失效。避免循环引用的过程如下所示:

  • MyObject类对象持有 Block
  • Block 持有__block变量

在这里插入图片描述

下面我们对使用block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained 修饰符避免循环引用的方法做个比较。
使用__block变量的优点如下:

  • 通过__block 变量可控制对象的持有期间
  • 在不能使用__weak修饰符的环境中使用__unsafe_unretained 修饰符即可(不必担心悬垂指针)
    在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在__block 变量中。使用__block变量的缺点如下:
  • 为避免循环引用必须执行Block
    存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用__block变量、__weak 修饰符或__unsafe_unretained修饰符来避免循环引用。

(九)copy/release

ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放复制的Block。这时我们用copy实例方法用来复制,用release实例方法来释放。

void (^blk_on_heap)(void) = [blk_on_stack copy];
[blk_on_heap release];

只要 Block 有一次复制并配置在堆上,就可通过retain实例方法持有。

[blk_on_heap retain];

但是对于配置在栈上的Block 调用retain实例方法则不起任何作用。

[blk_on_stack retain];

该源代码中,虽然对赋值给blk_on_stack的栈上的Block调用了retain 实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block。
另外,由于Blocks是C语言的扩展,所以在C语言中也可以使用Block语法。此时使用“Block_copy 函数”和“Block_release 函数”代替copy/release 实例方法。使用方法以及引用计数的思考方式与Objective-C 中的copy/release 实例方法相同。

void (^blk_on_heap)(void) = Block_copy(blk_on_stack);
Block_release(blk_on_heap);

Block_copy函数就是之前出现过的_Block_copy 函数,即Objective-C 运行时库所使用的为 C语言而准备的函数。释放堆上的Block时也同样调用Objective-C运行时库的Block_release 函数。另外,ARC无效时,__block 说明符被用来避免Block中的循环引用。这是由于当Block从栈复制到堆时,若Block 使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有block说明符的id类型或对象类型的自动变量,则被 retain。例如下面的源代码中,不管ARC有效无效都会引起循环引用,Block 持有 self,且 self持有 Block。

typedef void (^blk_t)(void);
@interface MyObject : NSObject {
    blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
    self = [super init];
    
    blk_ = ^{
        NSLog(@"self = %@", self);
    };
    return self;
}
- (void)execBlock {
    blk_();
}
-(void)dealloc {
    NSLog(@"dealloc");
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        id o = [[MyObject alloc] init];
        
        [o execBlock];
    }
    return 0;
}

这时我们使用__block变量来避免该问题。

- (id)init {
	self = [super init];
	__block id tmp = self;
	blk_ =^{
		NSLog(@"self=%@", tmp);
	};
	return self;
}

正好在ARC有效时能够同__unsafe_unretained 修饰符一样来使用。由于ARC 有效时和无效时__block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码是在ARC效情况下编译还是在ARC无效情况下编译。这一点要注意。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值