[OC学习笔记]Blocks

Posted Billy Miracle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[OC学习笔记]Blocks相关的知识,希望对你有一定的参考价值。

一、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 [OC学习笔记]Grand Central Dispatch

[OC学习笔记]接口与API设计

[OC学习笔记]GCD复习

[OC学习笔记]GCD复习

[OC学习笔记]Block三种类型

[OC学习笔记]objc_msgSend:方法快速查找