首页 > 代码库 > iOS中Blocks的介绍
iOS中Blocks的介绍
iOS中Blocks的介绍
1. 什么是Blocks
Blocks是C语言的扩充功能。如果用一句话来概括就是:带有自动变量的匿名函数。
第一次看见Blocks的时候,感觉很类似C语言的函数指针,尤其是Block类型变量,更是有极强的相似度。但Blocks不是一个指针,而是一个不带名字的函数,它允许开发者在两个对象之间将任意的语句当作数据进行传递,同时它又能获得上下文的信息(闭包Closure),所以在一些场合使用Blocks很方便简洁。
2. Block语法
我们先来看一个例子吧。
^int(int count){return count++;}
这个Block非常简单,就是把计数器加一,但麻雀虽小,五脏俱全,语法上一个元素都没漏掉。首先是^符号(插入符号caret),然后依次是是返回值类型,参数列表,表达式。
^ int (int count) {return count++;}
caret 返回参数 参数列表 表达式
齐全的Block就是这些内容了,不同的Block的表达式的复杂程度各异,但元素就是这么多了。
不过很多时候我们会遇到没有返回参数,或者没有传入参数,甚至既没有传入参数也没有返回参数的情况。这个时候Block可以省略相关的内容,就是说相对应的那一块不用写了。比如:
没有返回参数,可能就会写成:^(int count){printf("count=%d", count);}
没有参数列表,可能会写成:^int{return 1;}
既没有返回参数也没有参数列表,可能就会写成:^{printf("Blocks Demo!\n")};
Block还可以申明变量,Block变量一样可以用typedef来定义成新的类型,这种做法下和函数指针真的非常非常类似,仅仅是将*换成了^。举个例子:
int (^blk)(int) = ^int (int count) {return count+;};
熟悉C语言的人对这个都会比较熟悉。这里有一个要说明,上面的赋值语句右侧可以省略掉返回类型(猜测是这部分信息编译器已经可以确定,所以不再是必须提供的了)。这样,上面的语句也可以写成;
int (^blk)(int) = ^(int count) {return count+;};
如果使用typedef,就可以更清晰一点:
typedef int (^blk_t)(int);
blk_t blk = ^(int count) {return count+;};
3. 截获自动变量值
我们说Block是带自动变量的匿名函数,匿名函数现在已经知道了,下面就要看看“自动变量”了。Block可以访问在它之前申明的变量,但也有它特殊的地方,先看一个例子:
- int main(int argc, const char * argv[])
- {
- int val = 1;
- void (^blk)(void) = ^{printf("val=%d\n", val);};
- val = 2;
- blk();
- return 0;
- }
在这里,运行结果是val=1。请注意,虽然这个时候val的值已经变成了2,但Block里面仍然是1,也就是说,Block在定义时相当于对val这个变量照了张相,然后一直自己使用这张相片,不管val本身何去何从。
这个特性可以带来很大的便利(记下了定义时的上下文),有时是我们所需要的;但一不小心也很容易错,所以使用时需要注意。另外需要说明的是,Block里面不能改变val这个变量的值,如果你试图改变,编译器会报错,换句话说,Block里面,val就是只读的,而且值就是定义时的那个。
虽然记住上下文是个很棒的功能,但是有时我们需要外部上下文变化时,Block的内容也跟随改变或者要修改自动变量的值,这个就需要用到__block关键词了。继续上代码:
- int main(int argc, const char * argv[])
- {
- __block int val = 1;
- void (^blk)(void) = ^{printf("val=%d\n", val);};
- val = 2;
- blk();
- return 0;
- }
这段代码和上一段的区别仅仅是多了__block的声明,但运行结果就是val=2了,也就是说,Block能跟踪val的变化了。
这时,Block里面也可以改变val的值了,就是说,用了__block之后,val对于Block不再是只读的了,而是和自己定义的变量一样了。
在一个Block里面,往往两种变量都需要有,具体怎么使用,就看具体的情况了。
4. Block的使用
我总是觉得任何一种技术的出现总是用来解决某个问题的,也决定了在何种情况下使用该技术。Block应该如何使用呢?这是仁者见仁,智者见智的问题了,我接触最多的是在GCD里面。
个人的感觉这个东东主要用在回调里面,比如:网络连接成功后我应该把某个按钮激活之类的,Block使用起来简洁明快,如鱼得水。GCD实际上也是给出了系统的回调,所以就特别适合Block大显身手。
5. 其他
Block当然还有其他的一些内容,比如可以作为函数参数来传递,比如当自动变量是ObjC的对象时,虽然不能修改,但可以调用对象的方法,再比如,C语言的数组不能作为自动变量等等。但这些都不是Block主要的内容,最重要的还是灵活的使用。
深入Blocks分析
1.简介
从iOS4开始,苹果引入了这个C语言的扩充功能“Blocks”,在一些特定的场景下也是一把利刃。我前面一篇博客中初步介绍了Blocks这个东西,主要是语法的介绍(《iOS中Blocks的介绍》)。
我曾经看见了老外的一个系列的Blocks介绍,很有深度(A look inside blocks:Episode 1,A look inside blocks:Episode 2, A look inside blocks:Episode 3),里面深入到汇编的层次对Blocks的实现进行了分析。不过如果象我这样对于汇编不熟悉的人肯定也是很多的,理解起来十分痛苦,于是就想到从ObjC本身对Blocks进行的处理里面来入手分析,看看对于Blocks都悄悄做了什么。
2.环境
很简单,就是Xcode啦。使用的编译器是CLang,主要是利用了-rewrite-objc这个参数,把源文件转换成中间文件。这样就揭开了面纱的一角。我使用的clang编译器版本是:
Apple clang version 4.0 (tags/Apple/clang-421.0.60) (based on LLVM 3.1svn)
Target: x86_64-apple-darwin12.5.0
Thread model: posix
转成中间文件的命令是:clang -rewrite-objc 源文件
3. 例子1
- #include <stdio.h>
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", val);};
- val = 4;
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
代码的运行结果是:
in Block():val=2
in main():val=4, val1=5
这里我们可以看到Block对于自动变量的“快照”功能。由于转成中间文件之后发现长了很多,变成了100多行(不同的clang版本转换出来的文件还不同,不过实现部分代码还是一样的),下面的代码是相关部分的节选,主要说明苹果是如何实现的。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
- int main(int, const charchar **);
- struct __main_block_impl_0 {
- struct __block_impl impl;
- struct __main_block_desc_0* Desc;
- int val;
- __main_block_impl_0(voidvoid *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : 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) {
- int val = __cself->val; // bound by copy
- printf("in Block():val=%d\n", val);}
- static struct __main_block_desc_0 {
- unsigned long reserved;
- unsigned long Block_size;
- } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
- int main(int argc, const charchar * argv[])
- {
- int val = 2;
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, val);
- val = 4;
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
中间文件确实看起来复杂了不少,不过还是有脉络可循。
看main函数的内容,里面有个函数指针blk,这个就是指向Block的指针,所以难怪Block变量的声明和函数指针如此相像(就是把*换成^),编译器转换后就是同一个东西啊。
我们看blk这个函数指针,就是__main_block_impl_0这个结构体变量的指针,这个结构体变量此时已经存在,然后用__main_block_func_0等几个变量赋初值。我们可以看到__main_block_impl_0这个struct中有个val这个项,并且在这里也赋值了,这就是给变量照的“快照”,由于这个变量在这里被记录了,所以无论外面的val变量如何变化,Block运行时使用的值就始终是“快照”的值了。同时我们也注意到__main_block_impl_0这个struct中没有val1这个项,所以说明如果Block中不用到的自动变量是不会自动加入到结构体中的。
Block的运行就是运行__main_block_impl_0这个struct中的FuncPtr这个指针,这个在前面初始化的时候已经被赋值成__main_block_func_0了,所以这里也就是运行这个函数,并把自己的指针传入。这里我们的实现非常简单,就是一句printf语句。
4.例子2
- #include <stdio.h>
- int main(int argc, const charchar * argv[])
- {
- int __block val = 2;
- int val1 = 5;
- void (^blk)(void) = ^{printf("in Block():val=%d\n", ++val);};
- blk();
- printf("in main(): val=%d, val1=%d\n", val, val1);
- return 0;
- }
这个例子把自动变量val声明成了__block变量,这样语法上Block不是对val进行“快照”,而是会直接使用val变量,同时在Block内部val可读可写,不再是只读的了。
运行结果如下:
in Block():val=3
in main(): val=3, val1=5
同样展开成中间文件来看苹果的实现。
- struct __block_impl {
- voidvoid *isa;
- int Flags;
- int Reserved;
- voidvoid *FuncPtr;
- };
- #include <stdio.h>
- int main(int, const charchar **);
- struct __Block_byref_val_0 {
- voidvoid *__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(voidvoid *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; // bound by ref
- printf("in Block():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, 8/*BLOCK_FIELD_IS_BYREF*/);}
- static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*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(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
- int main(int argc, const charchar * argv[])
- {
- __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 2};
- int val1 = 5;
- void (*blk)(void) = (void (*)(void))&__main_block_impl_0((voidvoid *)__main_block_func_0, &__main_block_desc_0_DATA, (struct __Block_byref_val_0 *)&val, 570425344);
- ((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
- printf("in main(): val=%d, val1=%d\n", (val.__forwarding->val), val1);
- return 0;
- }
中间文件又变长了一些,除去我们已经了解的部分,我们可以看到自动变量val不再是直接加入到__main_block_impl_0里面,而是又变成了一个__Block_byref_val_0的struct结构体的指针。
main函数里面对于val的赋值已经变成了对这样一个数据结构的赋值,第一句上就把2赋给了__Block_byref_val_0里面的val项,然后在blk这个指针初始化的时候,把__Block_byref_val_0的结构体变量指针传入__main_block_impl_0。此后所有对于自动变量val的操作都变成对val.__forwarding->val的操作。这样就解决了Block内外变量同时变化的问题(在操作同一块内存)。
这里还看见__Block_byref_val_0里面有个__forwarding项,这个项是指向自身的一根指针。在blk指针初始化的时候我们把这个指针的值传入了__main_block_impl_0。
在__main_block_desc_0里面,多出了两个函数指针,分别用于copy和dispose,这两个函数这里也是自动生成的。
5.总结
综合前面的例子来看,Block的实现还是借助了C语言的函数指针来实现了,对于普通的自动变量,在Block声明时会快照内容存储;对于__block变量,则是生成一个数据结构来存储,然后取代所有访问这个变量的地方。
事实上,因为Block运行时完全可能自动变量的生命周期已经结束,所以Block对于内存的管理是很复杂的,会把内容从栈上copy到堆上(这也是copy和dispose函数的作用)。所以Block虽然威力巨大,但使用时也需要遵循一定的规则。
iOS中Blocks的介绍