[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语言函数定义相比,仅有两点不同。
- 没有函数名。
- 带有“^”。
第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号。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