Block对象是 C 级别的语法和运行时特性。它们和标准 C 函数很类似,但是除了可执行代码外,它们还可能包含了变量自动绑定(栈)或内存托管(堆)。所以一个block维护一个状态集(数据),它们可以在执行的时候用来影响程序行为。
你可以用 blocks来编写函数表达式,这些表达式可以作为 API 使用,或可选的存储,或被多个线程使用。Blocks作为回调特别有用,因为 block携带了进行回调所需要的执行代码和执行过程中需要的数据。
Blocks在 GCC和 Clang里面可用,它附带在 Mac OS X v10.6 里面的 Xcode开发工具里面。你可以在 Mac OS X v10.6 及其之后,和 iOS 4.0 及其之后上面使用blocks。Blocks运行时是开源的,你可以在 LLVM’s compiler-rt subproject repository(LLVM 的 RT 编译器的子项目库)里面找到它。Blocks同样作为标准 C 工作组出现在N1370:Apple’s Extensions to C(该文档同样包括了垃圾回收机制)。因为Objective-C和 C++都是从 C 发展而来,blocks被设计在三种语言上面使用(也包括Objective-C++)。(语法反应了这一目标)
你应该阅读该文档来掌握 block 对象是什么和如何在C,C++或Objective-C 上面使用它们来让你的程序更高效和更易于维护。
本文档结构
该文档包含了以下几个章节:
-
第一章 “Blocks入门”提供了一个对blocks的快速的和实际的介绍。
-
第二章 “概念概述”提供了对blocks概念的介绍。
-
第三章 “声明和创建Blocks”描述如何声明block变量,并实现该blocks。
-
第四章 “Blocks和变量”介绍了blocks和变量的交互,并定义__block存储类型修饰
符。
-
第五章 “使用Blocks”说明不同的使用模式。
第一章 Blocks入门以下部分使用实际的例子帮助你开始使用Blocks。
1.1声明和使用一个Block
使用^操作符来来声明一个block 变量和指示block 文本的开始。Block本身的主
体被{}包含着,如下面的例子那样(通常使用 C 的;符合指示block 的结束):
int multiplier =7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
注意 block可以使用相同作用域范围内定义的变量。
如果你声明一个 block 作为变量,你可以把它简单的作为一个函数使用:
int multiplier =7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
printf("%d", myBlock(3));
// prints "21"
1.2直接使用Block
在很多情况下,你不需要声明一个 block 变量;相反你可以简单的写一个内联(inline)的block 文本,它需要作为一个参数使用。以下的代码使用qsort_b 函数。qsort_b和标准 qsort_r函数类似,但是它使用 block作为最后一个参数。
char *myCharacters[3] = {"TomJohn", "George","Charles Condomine" };
qsort_b(myCharacters,3, sizeof(char *), ^(constvoid *l, constvoid *r) {
char *left = *(char **)l;
char *right = *(char **)r;
returnstrncmp(left, right, 1);
});
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
1.3 Cocoa的Blocks
在 Cocoa frameworks里面有部分方法使用 block作为参数,通常不是执行一个对象的集合操作,就是在操作完成的时候作为回调使用。下面的例子显示了如何通过NSArray的方法 sortedArrayUsingComparator:使用block。该方法使用一个参数,即block。为了举例说明,该情况下block 被定义为NSComparator 的局部变量:
NSArray *stringsArray = [NSArrayarrayWithObjects:
@"string 1",
@"String 21",
@"string 12",
@"String 11",
@"String 02",nil];
staticNSStringCompareOptions comparisonOptions =NSCaseInsensitiveSearch |NSNumericSearch |
NSWidthInsensitiveSearch |NSForcedOrderingSearch;
NSComparator finderSortBlock = ^(id string1,id string2) {
NSRange string1Range =NSMakeRange(0, [string1length]);
return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];
};
NSArray *finderSortArray = [stringsArraysortedArrayUsingComparator:finderSortBlock];
NSLog(@"finderSortArray: %@", finderSortArray);
/*
Output:
finderSortArray: (
"string 1",
"String 02",
"String 11",
"string 12",
"String 21"
)*/
1.4 __block变量
Blocks的最大一个特色就是可以修改相同作用域的变量。你可以使用__block存储类型修饰符来给出信号要修改一个变量。改编“Cocoa的 Blocks”所示的例子,你可以使用一个block 来计数多少个字符串和block 中只读变量currentLocal 相同:
NSArray *stringsArray = [NSArray arrayWithObjects:
@"string 1",
@"String 21",// <-
@"string 12",
@"String 11",
@"Strîng 21",// <-
@"String 02",nil];
NSLocale *currentLocale = [NSLocalecurrentLocale];
__blockNSUInteger orderedSameCount =0;
NSArray *diacriticInsensitiveSortArray = [stringsArraysortedArrayUsingComparator:^(id string1,id string2) {
NSRange string1Range =NSMakeRange(0, [string1length]);
NSComparisonResult comparisonResult = [string1compare:string2 options:NSDiacriticInsensitiveSearchrange: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",
"Str\U00eeng 21",
"Stri\U00f1g 21"
)
orderedSameCount: 2
*/
这会在“Blocks和变量”部分里面有更多的讨论。
第二章 概念概述
Block对象提供了一个使用 C 语言和 C 派生语言(如 Objective-C和 C++)来创建表达式作为一个特别(ad hoc)的函数。在其他语言和环境中,一个block对象有时候被成为“闭包(closure)”。在这里,它们通常被口语化为”块(blocks)”,除非在某些范围它们容易和标准 C 表达式的块代码混淆。
2.1 Block功能
一个 block就是一个匿名的内联代码集合体:
-
和函数一样拥有参数类型
-
有推断和声明的返回类型
-
可以捕获它的声明所在相同作用域的状态
可以和其他定义在相同作用域范围的blocks 进行共享更改
-
在相同作用域范围(栈帧)被销毁后持续共享和更改相同作用域范围(栈帧)的状
态
你可以拷贝一个 block,甚至可以把它作为可执行路径传递给其他线程(或者在自己的线程内传递给run loop)。编译器和运行时会在整个block生命周期里面为所有block引用变量保留一个副本。尽管 blocks在纯 C 和 C++上面可用,但是一个 block也同样是一个Objective-C 的对象。
2.2用处
Blocks通常代表一个很小、自包的代码片段。因此它们作为封装的工作单元在并发执行,或在一个集合项上,或当其他操作完成时的回调的时候非常实用。
Blocks作为传统回调函数的一个实用的替代办法,有以下两个原因:
-
它们可以让你在调用的地方编写代码实现后面将要执行的操作。
因此 Blocks通常作为框架方法的参数。
-
它们允许你访问局部变量。
而不是需要使用一个你想要执行操作时集成所有上下文的信息的数据结构来 进行回调,你可以直接简单的访问局部变量。
-
第三章 声明和创建Blocks3.1声明一个block的引用
Block变量拥有 blocks的引用。你可以使用和声明函数指针类似的语法来声明它们,除了它们使用 ^ 修饰符来替代* 修饰符。Block类型可以完全操作其他 C 系统类型。以下都是合法的 block声明:
void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int,char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
Blocks还支持可变参数(...)。一个没有使用任何参数的 block 必须在参数列表上面用void 标明。
Blocks被设计为类型安全的,它通过给编译器完整的元数据来合法使用 blocks、传递到blocks 的参数和分配的返回值。你可以把一个block 引用强制转换为任意类型的指针,反之亦然。但是你不能通过修饰符 * 来解引用一个block,因此一个block的大小是无法在编译的时候计算的。
你同样可以创建 blocks的类型。当你在多个地方使用同一个给定的签名的 block时,这通常被认为是最佳的办法。
typedeffloat (^MyBlockType)(float,float);
MyBlockType myFirstBlock =// ... ;
MyBlockType mySecondBlock = // ... ;
3.2创建一个block
你可以使用 ^修饰符来标识一个 block表达式的开始。它通常后面跟着一个被 ()包含起来的参数列表。Block的主体一般被包含在 {} 里面。下面的示例定义了一个简单的 block,并把它赋值给前面声明的变量(oneFrom)。这里block 使用一个标准 C的结束符; 来结束。
int (^oneFrom)(int);
oneFrom = ^(int anInt) {
return anInt -1;
}
如果你没有显式的给 block 表达式声明一个返回值,它会自动的从block 内容推断出来。如果返回值是推断的,而且参数列表也是void,那么你同样可以省略参数列表的void。如果或者当出现多个返回状态的时候,它们必须是完全匹配的(如果有必要可以使用强制转换)。
3.3全局blocks
在文件级别,你可以把 block作为全局标示符:
#import <stdio.h>
int GlobalInt =0;
int (^getGlobalInt)(void) = ^{return GlobalInt; };
第四章 Blocks和变量本文描述blocks 和变量之间的交互,包括内存管理。
4.1变量类型
在 block的主体代码里面,变量可以被使用五种方法来处理。
你可以引用三种标准类型的变量,就像你在函数里面引用那样:
-
全局变量,包括静态局部变量。
-
全局函数(在技术上而言这不是变量)。
-
封闭范围内的局部变量和参数。
Blocks同样支持其他两种类型的变量:
-
在函数级别是__block变量。这些在block里面是可变的(和封闭范围),并任何引
用 block的都被保存一份副本到堆里面。
-
引入const。
-
最后,在实现方法里面,blocks也许会引用Objective-C的实例变量。参阅“对象
和 Block变量”部分。
在 block里面使用变量遵循以下规则:
-
全局变量可访问,包括在相同作用域范围内的静态变量。
-
传递给block的参数可访问(和函数的参数一样)。
-
程序里面属于同一作用域范围的堆(非静态的)变量作为const变量(即只读)。
它们的值在程序里面的 block 表达式内使用。在嵌套 block 里面,该值在最近的
封闭范围内被捕获。
-
属于同一作用域范围内并被__block存储修饰符标识的变量作为引用传递因此是
可变的。
-
属于同一作用域范围内block的变量,就和函数的局部变量操作一样。
每次调用 block 都提供了变量的一个拷贝。这些变量可以作为 const来使用,或在block 封闭范围内作为引用变量。
下面的例子演示了使用本地非静态变量:
int x =123;
void (^printXAndY)(int) = ^(int y) {
printf("%d %d\n", x, y);
};
printXAndY(456);// prints: 123 456
正如上面提到的,在 block 内试图给 x 赋一个新值会导致错误发生:
int x =123;
void (^printXAndY)(int) = ^(int y) {
x = x + y; // error
printf("%d %d\n", x, y);
}
为了可以在 block内修改一个变量,你需要使用__block存储类型修饰符来标识该变量。参阅“__block存储类型”部分。
4.2 __block存储类型
你可以指定引入一个变量为可更改的,即读-写的,通过应用__block 存储类型修饰符。局部变量的__block的存储和 register、auto、static等存储类型相似,但它们之间不兼容。
__block变量保存在变量共享的作用域范围内,所有的 blocks和 block副本都声明或创建在和变量的作用域相同范围内。所以,如果任何 blocks副本声明在栈内并未超出栈的结束时,该存储会让栈帧免于被破坏(比如封装为以后执行)。同一作用域范围内给定的多个block 可以同时使用一个共享变量。
作为一种优化,block存储在栈上面,就像blocks本身一样。如果使用Block_copy拷贝了block 的一个副本(或者在Objective-C 里面给block 发送了一条copy 消息),变量会被拷贝到堆上面。所以一个__block变量的地址可以随时间推移而被更改。
使用__block的变量有两个限制:它们不能是可变长的数组,并且它们不能是包含有 C99 可变长度的数组变量的数据结构。
以下举例说明了如何使用__block变量:
__blockint x = 123; // x lives in block storage
void (^printXAndY)(int) = ^(int y) {
x = x + y;
printf("%d %d\n", x, y);
};
printXAndY(456);// prints: 579 456
// x is now 579
下面的例子显示了 blocks 和其他几个类型变量间的交互:
externNSInteger CounterGlobal;
staticNSInteger CounterStatic;
{
NSInteger localCounter =42;
__blockchar 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'
}
4.3对象(Object)和Block变量
Block 提供了支持Objective-C 和Objective-C++的对象,和其他blocks 的变量。
4.3.1Objective-C对象
在引用计数的环境里面,默认情况下当你在block 里面引用一个Objective-C 对象的时候,该对象会被retain。当你简单的引用了一个对象的实例变量时,它同样被retain。但是被__block存储类型修饰符标记的对象变量不会被 retain.注意:在垃圾回收机制里面,如果你同时使用__weak 和__block 来标识一个变量,那么该 block将不会保证它是一直是有效的。
如果你在实现方法的时候使用了 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);
};
4.3.2C++对象
通常你可以在 block内使用 C++的对象。在成员函数里面,通过隐式的导入this指针引用成员变量和函数,结果会很微妙。有两个条件可以让block 被拷贝:
如果你拥有__block存储的类,它本来是一个基于栈的C++对象,那么通常会使用copy的构造函数。
如果你在block 里面使用任何其他 C++基于栈的对象,它必须包含一个const copy的构造函数。该 C++对象使用该构造函数来拷贝。
4.3.3Blocks
当你拷贝一个 block时,任何在该 block里面对其他 blocks的引用都会在需要的时候被拷贝,即拷贝整个目录树(从顶部开始)。如果你有 block 变量并在该block 里面引用其他的block,那么那个其他的block 会被拷贝一份。
当你拷贝一个基于栈的 block 时,你会获得一个新的block。但是如果你拷贝一个基于堆的block,你只是简单的递增了该block 的引用数,并把原始的block 作为函数或方法的返回值。
第五章 使用Blocks
5.1调用一个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作为参数传递给一个函数或方法。在这种情况下,你通过需要创建一个”内联(inline)”的block。
5.2使用Block作为函数的参数
你可以把一个 block作为函数的参数就像其他任何参数那样。然而在很多情况下,你不需要声明 blocks;相反你只要简单在需要它们作为参数的地方内联实现它们。下面的例子使用qsort_b 函数。qsort_b和标准 qsort_r函数类似,但是它最后一个参数用block.
char *myCharacters[3] = {"TomJohn", "George","Charles Condomine" };
qsort_b(myCharacters,3, sizeof(char *), ^(constvoid *l, constvoid *r) {
char *left = *(char **)l;
char *right = *(char **)r;
returnstrncmp(left, right, 1);
});
// Block implementation ends at "}"
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }
注意函数参数列表包含了一个 block。
下一个例子显示了如何在 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\n", i);
});
5.3使用Block作为方法的参数
Cocoa提供了一系列使用 block的方法。你可以把一个 block作为方法的参数就像其他参数那样。
下面的例子确定数组前面五个元素中第一个出现在给定的过滤器集中任何一个
的下标。
NSArray *array = [NSArrayarrayWithObjects:@"A", @"B",@"C", @"A",@"B", @"Z",@"G",@"are", @"Q",nil];
NSSet *filterSet = [NSSetsetWithObjects: @"A",@"Z", @"Q",nil];
BOOL (^test)(id obj,NSUInteger idx, BOOL *stop);
test = ^ (id obj,NSUInteger idx, BOOL *stop) {
if (idx <5) {
if ([filterSetcontainsObject: obj]) {
returnYES;
}
}
returnNO;
};
NSIndexSet *indexes = [arrayindexesOfObjectsPassingTest:test];
NSLog(@"indexes: %@", indexes);
/*
Output:
indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]
*/
下面的例子确定一个 NSSet 是否包含一个由局部变量指定的单词,并且如果条件成立把另外一个局部变量(found)设置为 YES(并停止搜索)。注意到found 同时被声明为__block变量,并且该 block是内联定义的:
__blockBOOL found = NO;
NSSet *aSet = [NSSetsetWithObjects: @"Alpha",@"Beta", @"Gamma",@"X", nil];
NSString *string =@"gamma";
[aSet enumerateObjectsUsingBlock:^(id obj,BOOL *stop) {
if ([objlocalizedCaseInsensitiveCompare:string] ==NSOrderedSame) {
*stop = YES;
found = YES;
}
}];
// At this point, found == YES
5.4拷贝Blocks
通常,你不需要 copy(或retain)一个block.在你希望block 在它被声明的作用域被销毁后继续使用的话,你子需要做一份拷贝。拷贝会把 block 移到堆里面。
你可以使用 C 函数来 copy 和release 一个block:
Block_copy();
Block_release();
如果你使用 Objective-C,你可以给一个block 发送copy、retain和 release(或autorelease)消息。
为了避免内存泄露,你必须总是平衡Block_copy()和Block_release()。你必须平衡copy或 retain和 release(或autorelease)--除非是在垃圾回收的环境里面。
5.5需要避免的模式
一个 block的文本(通常是^{...})是一个代表block 的本地栈数据结构地址。因此该本地栈数据结构的作用范围是封闭的复合状态,所以你应该避免下面例子显示的模式:
void dontDoThis() {
void (^blockArray[3])(void);// an array of 3 block references
for (int i =0; i < 3; ++i) {
blockArray[i] = ^{ printf("hello, %d\n", 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\n", i); };
// WRONG: The block literal scope is the "then" clause
}
// ...
}
5.6调试
你可以在 blocks里面设置断点并单步调试。你可以在一个 GDB 的对话里面使用
invoke-block来调用一个 block。如下面的例子所示:
$ invoke-block myBlock 1020
如果你想传递一个 C 字符串,你必须用引号括这它。例如,为了传递this string给doSomethingWithString 的block,你可以类似下面这样写:
$ invoke-block doSomethingWithString "\"this string\""
结束语
Block是 iOS 4.0之后添加的新特性支持。本人亲测感觉使用 Block最大的便利就是简化的回调过程。以前使用 UIView的动画,进程要控制动画结束后进行相应的处理。iOS 4.0之后,UIView新增了对 Block的支持,现在只要使用简单的一个 Block代码就可以在写动画的代码部分直接添加动画结束后的操作。还有就是在使用
Notification时候 Block也非常有帮助。反正多用就可以体会到 Block的优美了。
对了,使用 Block要谨记别造成对象互相引用对方导致引用计数进入一个循环导致对象无法被释放。iOS 5.0之后的 ARC也是无法解决该潜在的互相引用的问题的。所以写 Block的时候要注意这点。因为 Block往往在后台自动对一些它引用了的对象进行 retain操作。具体形式这里就不距离了,大家在使用的时候多体会一下。