最近的工作中比较频繁的用到了Block,不在是以前当做函数指针的替代或者某些API只有Blocks形式的接口才不得已用之了,发现自己对其了解还是太浅,特别是变量的生存期,按惯例还是翻译官方文档,原文链接。
介绍
Block 对象是C语言层面的语法,也是一个运行时特性. 它们很类似与标准的C函数,但是除了可执行的代码,它们还包含了与自动(栈)或托管(堆)的内存所绑定的变量。因此一个block维护了一系列的状态(即数据),在执行时会改变代码的行为。
你可以使用blocks编写函数表达式当参数传入API,也可以将其保存下来用于多线程。Blocks在回调(回调的概念)中非常有用,因为block不仅包含着回调时需要执行的代码,还包含了执行代码时需要的数据。
你可以在Mac OS X 10.6和iOS 4.0之后的版本上的GCC附带的Clang上使用。blocks运行时库是开源的,见此 LLVM’s compiler-rt subproject repository.Blocks也已经呈现给了C标准工作组,见 N1370: Apple’s Extensions to C (其中包含了垃圾回收). 正如Objective-C和C++都是C的衍生语言,blocks也被设计成可以在这三种语言中使用 (如同Objective-C++). (语法也可以表现出其目标).
你应该阅读本文档来学习block的相关知识,以将block用于C,C++,Objective-C中,并提高你的程序的效率可可维护性。
文档结构
本文档包括以下章节:
-
"Blocks入门" 提供了快速,使用的blocks简介
-
“整体概念” 提供了blocks在概念上的介绍
-
"声明和创建Blocks" 为您展示如何声明block变量及实现blocks
-
"Blocks和变量" 描述blocks和变量之间的相互作用,
__block
修饰符的作用 -
"使用Blocks" 详解各种用法范式
Blocks入门
下面章节使用实际的例子帮助您入门blocks
声明和使用Block
你要使用^操作符去声明一个block变量,^也是标示着一段block文字的开始。block的实体包含在{}中,如下所示(形同C,;表示语句的终结):
- int multiplier = 7;
- int (^myBlock)(int) = ^(int num) {
- return num * multiplier;
- };
注意block可以使用其定义范围内的变量.
如果你把block声明为一个变量,你可以把它当一个函数(function,本文中特指C语言形式的函数)一样调用:
- int multiplier = 7;
- int (^myBlock)(int) = ^(int num) {
- return num * multiplier;
- };
- printf("%d", myBlock(3));
- // prints "21"
直接使用Block
在很多场景下,你不需要定义一个block变量,作为替代,仅仅只需要在需要block参数的地方写block文字即可。下例使用了qsort_b
函数. qsort_b
很类似标准的 qsort_r
函数,不过它使用block作为最后一个参数.
- char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
- qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
- char *left = *(char **)l;
- char *right = *(char **)r;
- return strncmp(left, right, 1);
- });
- // myCharacters is now { "Charles Condomine", "George", "TomJohn" }
Cocos中的Blocks
许多 Cocoa frameworks 中的方法(method,特指Objecitve-C的方法即[])使用block作为参数, 常见于对集合中对象的操作或一个操作完成之后的回调. 下例展示和如何在NSArray
的方法 sortedArrayUsingComparator:
中使用block。该方法使用一个block作为参数. 例子中的block被定义为一个 NSComparator
类型的局部变量:
- NSArray *stringsArray = [NSArray arrayWithObjects:
- @"string 1",
- @"String 21",
- @"string 12",
- @"String 11",
- @"String 02", nil];
- static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch |
- NSWidthInsensitiveSearch | NSForcedOrderingSearch;
- NSLocale *currentLocale = [NSLocale currentLocale];
- NSComparator finderSortBlock = ^(id string1, id string2) {
- NSRange string1Range = NSMakeRange(0, [string1 length]);
- return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];
- };
- NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];
- NSLog(@"finderSortArray: %@", finderSortArray);
- /*
- Output:
- finderSortArray: (
- "string 1",
- "String 02",
- "String 11",
- "string 12",
- "String 21"
- )
- */
blocks有一个强大的特性,即它可以修改其当前词法范围内的变量. 只要对变量加上 __block
存储修饰符. 稍微修改上面的例子, 使用一个block变量去统计下例中有多少字符串是相同的。例子中的block还是直接使用的,并用到了叫 currentLocale
的只读变量:
- NSArray *stringsArray = [NSArray arrayWithObjects:
- @"string 1",
- @"String 21", // <-
- @"string 12",
- @"String 11",
- @"Strîng 21", // <-
- @"Striñg 21", // <-
- @"String 02", nil];
- NSLocale *currentLocale = [NSLocale currentLocale];
- __block NSUInteger orderedSameCount = 0;
- NSArray *diacriticInsensitiveSortArray = [stringsArray sortedArrayUsingComparator:^(id string1, id string2) {
- NSRange string1Range = NSMakeRange(0, [string1 length]);
- NSComparisonResult comparisonResult = [string1 compare:string2 options:NSDiacriticInsensitiveSearch range:string1Range locale:currentLocale];
- if (comparisonResult == NSOrderedSame) {
- orderedSameCount++;
- }
- return comparisonResult;
- }];
- NSLog(@"diacriticInsensitiveSortArray: %@", diacriticInsensitiveSortArray);
- NSLog(@"orderedSameCount: %d", orderedSameCount);
- /*
- Output:
- diacriticInsensitiveSortArray: (
- "String 02",
- "string 1",
- "String 11",
- "string 12",
- "String 21",
- "StrU00eeng 21",
- "StriU00f1g 21"
- )
- orderedSameCount: 2
- */
整体概念
Block对象提供了创建特殊函数的方法,函数体可用C,Objective-C,C++等C类的语言做表达式. 在其他的语言环境中,block变量可能会被叫做"闭包(closure)", 而在这里,除非和标准C术语的一段(block)代码混淆的情况之外,一般称为"blocks"。
Block 功能性
一个block就是一块匿名的代码块:
- 和函数一样有含类型的参数列表
- 有直接声明或可推断出的返回值
- 可以获得当前词法范围的状态
- 有能力修改当前词法范围的状态
- 可以和当前词法范围的其他block共同修改状态
- 可以持续共享和修改当前词法范围的状态,甚至在当前词法范围销毁之后
你可以copy一个block还可以将其传到别的线程以延后执行 (或本线程的执行循环里). 编译器和运行时会把block里用到的所有变量保存到该block的所有拷贝的生存期后。尽管blocks可以由纯C或C++写成,但block本身始终是一个Objective-C变量.
用法
Blocks一般都是小段的,自成体系的代码块. 因此,特别适合用在封转并行操作所需的数据,或用于集合中,以及操作完成后的回调.
Blocks基于两大理由,是传统回调函数的优秀替代品:
-
允许你把具体实现代码写在调用该方法的地方.
Blocks也经常是framework的方法参数.
- 可以访存局部变量. 不需要像以前的回调一样,把在操作后所有需要用到的数据封装成特定的数据结构, 你完全可以直接访问局部变量.
声明和创建Blocks
声明Block引用
Block变量保持blocks的引用. 声明block的语法类似于声明函数指针,区别之处在于使用^而不是*.block的类型取决于C的类型系统. 下面都是有效的block变量声明:
- void (^blockReturningVoidWithVoidArgument)(void);
- int (^blockReturningIntWithIntAndCharArguments)(int, char);
- void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
Blokcs也支持可变参数即(...). 如果不带参数则必须在参数列表中指定 void
.
Blocks被设计为完全的类型安全,编译器可由一整套的元数据去判断blocks,blocks的参数,及返回值的有效性(这句没太看懂,原文是Blocks are designed to be fully type safe by giving the compiler a full set of metadata to use to validate use of blocks, parameters passed to blocks, and assignment of the return value)。 你可以把block强制转换为任意类型的指针,反之亦然。不过,你不能像对指针一样用*运算符对block进行解引用(dereference)操作,因为block的大小无法在编译时算出.
你也可以创建blocks的类型,在几个地方用到同样函数签名的block时,这是一种很好的做法:
- typedef float (^MyBlockType)(float, float);
- MyBlockType myFirstBlock = // ... ;
- MyBlockType mySecondBlock = // ... ;
创建Block
使用 ^
操作符标示出block表达式的字面开始部分. 后面跟随着包含参数列表的().block主体则是包含在{}之中。 下例展示了如何定义一个简单的block,并将其赋值给之前定义的变量 (oneFrom
).最后由常规的 ;
也是c语言的语句结束符结束
- int (^oneFrom)(int);
- oneFrom = ^(int anInt) {
- return anInt - 1;
- };
如果你不想显式的声明block的返回值,也可以由block内容自动推断出来。如果返回值是可推断的,并且参数列表为 void
, 你也可以省略参数列表. 当有多个返回语句时,它们必须是类型一直的(必要时会进行强行转换).
全局Blocks
在文件层面上,你可以使用block当全局的字面文字:
- #import <stdio.h>
- int GlobalInt = 0;
- <p>int (^getGlobalInt)(void) = ^{ return GlobalInt; };</p>
Blocks和变量
本部分阐述blocks和变量之间的交互作用,包括内存管理.
变量的类型
在block的主体代码块中,变量可以分为五种.
你可以引用三种标准类型的变量,就如同函数传参:
-
全局变量,包括静态局部变量
-
全局函数 (理论上来说不是变量)
-
作用域(enclosing scope)内的局部变量和参数
Blocks还支持另外两种类型的变量:
-
在函数级别的
__block
变量. 它们在block中是可变的 (同时也在作用域中可变),如果有block被拷贝到了堆(heap)上,则它们也会被保存. -
const
imports.
最后,在一个方法的具体实现中,blocks可以引用Objective-C的实体变量,见 “对象和Block变量.”
block使用变量适用如下规则:
-
全局变量是可访存的,包括作用域内的静态变量.
-
block的参数是可访存的 (和函数的参数一样).
-
作用域内的局部栈(非静态)变量被当做
const
变量.它们的值以block表达式在程序的点为准. 在嵌套blocks中,则是里该block最近的作用域中的值为准.
-
词法作用域中的局部变量有
__block
存储修饰符的,是按引用传递并可以修改.所有的变动都反应到作用域中,包括其他在本作用域内定义的block中做的修改.
-
在block内部声明的局部变量,就和函数内声明的变量一样.
每次调用block都产生变量的新的拷贝,这些变量轮流被当做
const
或按引用传递的变量用在block内部.
下例使用局部非静态变量:
- int x = 123;
- void (^printXAndY)(int) = ^(int y) {
- printf("%d %d ", x, y);
- };
- printXAndY(456); // prints: 123 456
必须要注意,如果想在block内改变x的值,会导致错误:
- int x = 123;
- void (^printXAndY)(int) = ^(int y) {
- x = x + y; // error
- printf("%d %d ", x, y);
- };
如果想要在block内改变变量,你要使用 __block
类型的存储修饰符,详见“ __block 存储类型.”
The __block Storage Type
你可以指明一个导入的变量为可变的即可读写的,只需要使用 __block
类型存储修饰符. __block
存储类型类不同于 register
, auto
, 但和static
存储类型一样,对于局部变量提供了可变的能力.
__block
变量生存于存储区内,并供当前词法范围的所有blocks共享使用. 因此,该存储区将存活到block栈frame销毁之后,甚至在其他拷贝或声明了这些block的block销毁后 (比如压到队列中供后续执行). 在给定的词法范围里的多个blocks可以同时使用一个共享的变量.(这段我整个没看懂,原文是__block
variables live in storage that is shared between the lexical scope of the variable and all blocks and block copies declared or created within the variable’s lexical scope. Thus, the storage will survive the destruction of the stack frame if any copies of the blocks declared within the frame survive beyond the end of the frame (for example, by being enqueued somewhere for later execution). Multiple blocks in a given lexical scope can simultaneously use a shared variable.)
作为优化, block变量和block本身一样开始是存储在栈上. 但如果用Block_copy
(如果是Objecitve-C环境下, 可以直接向block发送 copy
)对block进行拷贝, 变量就会拷贝到堆上. 因此 __block
变量的地址是可以改变的.
对于 __block
变量还有两个更严格的限制: 不能是变长数组,也不能是含有C99的变长数组的结构体.
下例示范如何使用 __block
变量:
- __block int x = 123; // x lives in block storage
- void (^printXAndY)(int) = ^(int y) {
- x = x + y;
- printf("%d %d ", x, y);
- };
- printXAndY(456); // prints: 579 456
- // x is now 579
- extern NSInteger CounterGlobal;
- static NSInteger CounterStatic;
- {
- NSInteger localCounter = 42;
- __block char localCharacter;
- void (^aBlock)(void) = ^(void) {
- ++CounterGlobal;
- ++CounterStatic;
- CounterGlobal = localCounter; // localCounter fixed at block creation
- localCharacter = 'a'; // sets localCharacter in enclosing scope
- };
- ++localCounter; // unseen by the block
- localCharacter = 'b';
- aBlock(); // execute the block
- // localCharacter now 'a'
- }
Blocks支持Objecitve-C和C++对象,还包括其他的可以看成变量的blocks.
Objective-C对象
在引用计数的环境下,默认情况如果你引用了一个Objective-C对象,它将会被retain,即使你只是使用了这个对象的实体变量. 如果对象使用了 __block
存储修饰符,则不会被retain.
如果你在方法中使用了block,并且使用到了对象的实体变量,那么内存管理的规则将会更微妙:
-
如果你使用了改实体变量的引用,则
self
将被retain; -
如果你是按指访问该实体变量,则存储那个实体变量将被retain.
下例展示这两种情况的差别:
- dispatch_async(queue, ^{
- // instanceVariable is used by reference, self is retained
- doSomethingWithObject(instanceVariable);
- });
- id localVariable = instanceVariable;
- dispatch_async(queue, ^{
- // localVariable is used by value, localVariable is retained (not self)
- doSomethingWithObject(localVariable);
- });
你可以在block内使用C++变量. 在成员函数中,对成员变量和函数的引用实际上都是隐式使用了 this
指针,故而都是可变的. 当block被拷贝的时候,有两种情况需要留心:
-
如果你使用的是栈基(stack-based)的C++变量,并且有
__block
存储修饰符, 通常使用拷贝构造函数来构造对象. -
除此以外的栈基C++对象, 则必须要有const拷贝构造函数(形如 Object(const Object&o)),拷贝这些对象时将使用该方法.
Blocks
当拷贝一个block,该block内所有引用到的其他block都将被拷贝,如果改block使用到的block变量里引用到了别的block,则别的block也将被拷贝.
当拷贝一个栈基block,你将得到一个新block. 如果拷贝一个堆基的block,则仅仅是增加其引用计数,在拷贝函数和方法返回后还会降回去.
使用Blocks
调用Block
把一个block声明为一个变量,你就可以把它当做函数一样用,如下所示:
- int (^oneFrom)(int) = ^(int anInt) {
- return anInt - 1;
- };
- printf("1 from 10 is %d", oneFrom(10));
- // Prints "1 from 10 is 9"
- float (^distanceTraveled) (float, float, float) =
- ^(float startingSpeed, float acceleration, float time) {
- float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);
- return distance;
- };
- float howFar = distanceTraveled(0.0, 9.8, 1.0);
- // howFar = 4.9
使用Block作为函数参数
你可以传一个block当参数给函数,就和其他类型的参数一样. 很多情况,你都没必要声明blocks, 而是简单的在哪里需要就哪里直接创建. 下例展示使用qsort_b
函数. qsort_b
类似于标准的 qsort_r
函数,不过只是把最后一个参数改为block.
- char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
- qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
- char *left = *(char **)l;
- char *right = *(char **)r;
- return strncmp(left, right, 1);
- });
- // Block implementation ends at "}"
- // myCharacters is now { "Charles Condomine", "George", "TomJohn" }
下例演示如何在 dispatch_apply
函数中使用block. dispatch_apply
声明如下:
- void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
函数将block投递到调度队列中供多次调用. 一共三个参数,其中第一个是调用总次数,第二个是投递到的队列,最后是block本身,而这个block带有一个参数,即当前被调用的次数.
可以仅仅使用 dispatch_apply
去打印打钱的调度下标,如下:
- #include <dispatch/dispatch.h>
- size_t count = 10;
- dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- dispatch_apply(count, queue, ^(size_t i) {
- printf("%u ", i);
- });
使用Block作为方法参数
Cocoa 提供了很多使用blocks的方法, 你可以传递block作为参数,就像调用其他的方法一样.
下例展示如何使用给定的过滤条件为数组排出前五个元素.
- NSArray *array = [NSArray arrayWithObjects: @"A", @"B", @"C", @"A", @"B", @"Z",@"G", @"are", @"Q", nil];
- NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];
- BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);
- test = ^ (id obj, NSUInteger idx, BOOL *stop) {
- if (idx < 5) {
- if ([filterSet containsObject: obj]) {
- return YES;
- }
- }
- return NO;
- };
- NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];
- NSLog(@"indexes: %@", indexes);
- /*
- Output:
- indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]
- */
下例是判断一个 NSSet
对象是否包含了一个局部变量,并设置另一个局部变量(found
),在查找到的情况下 为 YES
(并停止查找) . 注意 found
被声明为了 __block
变量, 且block是用内联方式定义的:
- __block BOOL found = NO;
- NSSet *aSet = [NSSet setWithObjects: @"Alpha", @"Beta", @"Gamma", @"X", nil];
- NSString *string = @"gamma";
- [aSet enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
- if ([obj localizedCaseInsensitiveCompare:string] == NSOrderedSame) {
- *stop = YES;
- found = YES;
- }
- }];
- // At this point, found == YES
拷贝Blocks
一般而言,你没有表去copy(或retain)一个block. 除非你希望使用该block在当前声明的范围销毁之后. 拷贝会将block移到堆(heap)中.
你可以使用C函数去拷贝或释放blocks:
- Block_copy();
- Block_release();
copy
, retain
, 和 release
(也包括 autorelease
) 消息.为了避免内存泄露, Block_copy()
和Block_release()
必须要对应使用. 同样的也要对应 copy
或 retain
和 release
(或 autorelease
)的使用.除非是在垃圾回收的环境.
应避免的做法
block文本(即, ^{ ... }
) 是表示block的局部栈数据结构(stack-local data structure)的地址. 所以这些栈数据仅在当前声明的范围内有效,必须避免如下的使用:
- void dontDoThis() {
- void (^blockArray[3])(void); // an array of 3 block references
- for (int i = 0; i < 3; ++i) {
- blockArray[i] = ^{ printf("hello, %d ", i); };
- // WRONG: The block literal scope is the "for" loop
- }
- }
- void dontDoThisEither() {
- void (^block)(void);
- int i = random():
- if (i > 1000) {
- block = ^{ printf("got i at: %d ", i); };
- // WRONG: The block literal scope is the "then" clause
- }
- // ...
- }
调试
你可以在blocks中设断点并单步跟踪. 你也可以在GDB里直接用 invoke-block
命令调用blocks,如下所示:
- $ invoke-block myBlock 10 20
this string
传给 doSomethingWithString
block, 得这么写:- $ invoke-block doSomethingWithString ""this string""