介绍
block对象是C语言的语法和运行时特性。它与标准的C函数很相似,但是在运行代码的同时它还可以包含栈中或堆中的变量。因此,block对象可以在执行时保留一些可以用来影响运行行为的状态(数据)。
你可以使用block对象去组成可以被传递给API、可选的储存、或是被多线程使用的函数表达式。block在回调是尤其有用,因为block同时包含了用来执行的代码和在执行时需要的数据。
因为Objective-C和C++都是源自C语言的,所以在这三种语言中block都是可以使用的。
开始使用block
声明并使用block
使用^
操作符来声明一个block变量,同时^
操作符也指示了block字面量的开始。block的方法体由{}
包住,如下例所示:
int multipier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multipier;
}
下图是这个例子的解释:
值得注意的是,这个block是可以使用与其定义在相同作用域内的变量的。
如果你将一个block声明成一个变量,你就可以把这个变量当做函数一样使用。
直接使用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" }
在Cocoa中的block
许多在Cocoa框架中的方法都将block作为一个参数,无论是在对象的集合中执行一个操作还是当一个操作完成时将block当做回调。如下的例子展示了如何在NSArray的sortedArrayUsingComparator:
方法中使用block,这个方法只有一个参数——block。为做力争,在这个例子中block被定义成了一个NSComparator
的本地变量。
NSArray *stringsArray = @[ @"string 1",
@"String 21",
@"string 12",
@"String 11",
@"String 02" ];
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"
)
*/
__block变量
block的一个强大的特性就是它可以改变与其在同一作用域中的变量。你可以通过在变量钱添加__block
存储类型来告诉block这个变量是可以被修改的。调整一下 Blocks with Cocoa中的那个例子,可以使用了一个block变量来计算多少个字符串是相等的,调整后的代码如下。(在这个例子中没有定义block变量,而是直接使用了block)
NSArray *stringsArray = @[ @"string 1",
@"String 21", // <-
@"string 12",
@"String 11",
@"Strîng 21", // <-
@"Striñg 21", // <-
@"String 02" ];
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",
"Str\U00eeng 21",
"Stri\U00f1g 21"
)
orderedSameCount: 2
*/
概念概述
block对象为C语言与源于C语言的Objective语言、C++语言提供了一种创建函数体特殊的方式。在其他语言环境中,block对象有时也被称作是“闭包”。
block的功能
block是一种这样的匿名内联的代码集合:
- 与函数一样拥有类型参数列表
- 拥有可推断的或者声明的返回类型
- 可以从定义它的词法作用域内捕获状态
- 可以选择性的修改词法作用域内的状态
- 可以与其他定义在相同的词法作用域中的block对象分享潜在的修改(__block??)
- 在词法作用域(栈中的)销毁之后可以持续修改和分享在其词法作用域内定义的状态
你可以复制、甚至将其传递到另一个线程来推迟其运行。编译器和运行时系统负责安排所有在block中引用的变量在block的生命周期中的维持。尽管在纯C和C++中block都是可以使用的,但是block也经常是一个Objective-C对象。
用法
block经常表示小的、自包含的代码块。因此,他们在并行执行中作为一种封装的工作单元、遍历集合中的元素、或者当一个操作完成时执行回调时尤其有用。
block在替代传统的回调函数时很有用的原因主要有一下两点:
- block允许在在调用方法实现的上下文中执行的调用点上写代码。
因此block在框架中的方法中经常作为阐述
- block允许存取局部变量。
与其使用需要一种包含你需要执行操作所有上下文信息的数据结构的回调,你可以通过使用block来直接存取局部变量。
声明和使用block
声明一个block引用
block变量拥有对block对象的引用。处理需要将*替换成^外,你可以通过使用类似于声明函数指针的语法来声明他们。block类型可以与其他C 中的类型进行互操作。下面的示例均为block变量的声明:
void (^blockReturnVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
block同样也支持可变参数。没有任何参数的block必须在参数列表里明确写明void。
block是完全类型安全的,这是通过一整套完整的元数据来验证block的用法,传递给block的参数,赋给返回值的类型来保证的。你可以将一个block引用转换为任意一种类型的指针,反之亦然。然而,你不能通过解引用操作符(*)来对一个block引用进行解引用(这样,块的大小在编译时无法计算。)
你可以为block创建一种类型,如下所示:
typedef float (^MyBlockType)(float,float);
MyBlockType myFitrstBlock = // ...;
MyBlockType mySecondBloak = // ...;
创建一个block
使用^
操作符来指明block字面量表达式的开始。^
操作符之后中的()
可能会包含一个参数列表。block体是由{}
括起来的,下面的例子定义了一个简单的block并将其赋给了一个之前声明的变量(oneFrom):
float (^oneFrom)(float);
oneFrom ^(float aFloat) {
float result = aFloat - 1.0;
return result;
}
如果你不显示的声明block表达式的返回值,返回值就会通过block的内容来进行自动的推断。如果返回值是被推断出来的同时参数列表是void
,这时就可以同样将void
参数进行省略。如果多个返回语句存在,他们必须精确匹配(必要时使用转化)。
全局block
在文件层次上,你可以使用一个block作为全局的block。
#import<stdio.h>
int GlobalInt = 0;
int (^getGlockInt)(void) = ^{return GloabalInt;};
block与变量
这一小节介绍了block与变量之间的包括内存管理在内的交互
变量的类型
在block对象的代码体重,变量可以分为5种。
你可以引用三种标准类型的变量,就像你在函数里边使用的一样:
- 全局变量,包括静态的局部变量
- 全局函数
- 在作用域范围内的局部变量和参数
block还支持其他两种类型的变量:
- 在函数层次上是
__block
变量。他们在block的作用域中是可变的,同时可以如果任何引用块被拷贝到堆上这些变量可以被保存。 - import导入的
const
变量。
最后,在方法实现时,block可以引用Objective-C实例变量(参见 对象与block变量一节 )
下面的规则适用于block中的变量:
- 全局变量可以被存取,包括封闭词法作用域内存在的静态变量。
- 传入到block中的参数是可被存取的(就像函数中的参数一样)
- 词法作用域的栈中的变量被当做
const
被block捕获的。他们的值是在程序内的设置block表达式时被捕获。在嵌套block中,值是从最近的封闭范围捕获的。 - 被
__block
修饰的变量在block中是可以被修改的(是引用)。任何改变都会反应到到词法作用域中,包括其他任何在相同的词法作用域中定义的block。这些将会在__block存储类型
中做详细介绍。 - 在block的作用域中声明的局部变量就像函数中的局部变量是一样的。每一次对block的调用都会提供这些局部的一个拷贝。这些变量可以作为常量或在封闭的块中引用变量。
下面的例子说明了局部的非静态变量的使用方法:
int x = 123;
void (^printXAndY)(int) = ^(int y) {
print("%d %d\n", x, y);
}
printXAndY(456);
如果尝试去给x赋一个心智将会产生一个错误:
int x = 123;
void (^printXAndY)(int) = ^(int y) {
x = x + y;
print("%d %d\n", x, y);
}
为了让变量可以在block中被改变,你可以使用__block
存储类型。
__block存储类型
你可以通过应用__block
来指定一个被引入的变量是可变的(可读写)。__block
存储类型有点像register
,auto
,static
,但是与这些类型是互斥的。
存储__block
变量的内存是被__block
变量的词法域和所有的block、在__block
变量的词法域中的block拷贝、创建的block所共享的。因此,如果任何在栈中的这个block的拷贝在栈帧结束之后依然存活,这个内存会在栈帧被销毁后继续留存。在一个给定的词法域中的多个block可以同时使用一个共享的变量。
为了优化,block变量的存储一开始是在栈中的,就像block本身一样。如果对一个block使用了copy,这个变量就会被拷贝到堆上。因此,一个__block变量的地址可以随时间而改变。
这里还有两个对__block
变量更高的显示:它们不能是可变长的array,不能是包含C99中可变长arry的结构体。
下面的例子说明了__block
变量的用法:
__block int x = 123; // x 存活在block存储中
void (^printXAndY)(int) = ^(int y) {
x = x + y;
printf("%d %d\n",x , y);
}
printXAndY(456); // 输出: 579 456
// x现在是579
下面的例子展示了block与多种类型的变量之间的交互:
extern NSInteger CounterGlobal;
static NSInterger CounterStatic;
{
NSInteger localCounter = 42;
__block char localCharacter;
void (^aBlock)(void) = ^(void) {
++ CounterGlobal;
++ CounterStatic;
CounterGlobal = localCounter;
localCharacter = 'a';
}
++localCounter;
localCounter = 'b';
aBlock();
}
对象与block变量
Objective-C对象
当一个block被拷贝的时候,block就创建了对block中使用的对象变量的强引用。如果你在一个方法的实现中使用了block:
- 如果你使用其引用来存取一个实例变量,就产生了对self的强引用
- 如果你使用其值来存取实例变量,就产生了对这个值的强引用
(具体还是看下面的代码吧)
下面的代码介绍了这两种不同的情况:
dispatch_async(queue, ^{
// instanceVariable is used by reference, a strong reference is made to self
doSomethingWithObject(instanceVariable);
});
id localVariable = instanceVariable;
dispatch_async(queue, ^{
/*
localVariable is used by value, a strong reference is made to localVariable
(and not to self).
*/
doSomethingWithObject(localVariable);
});
block
当你拷贝一个block,如果有必要的话,任何在这个block中对其他block的引用也会被复制。如果你有一个block变量同时你在这个block中引用了其他block(aBlock),这个block(aBlock)也会被复制。
使用block
调用一个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。
将block作为函数的参数
你可以传递一个block作为函数的参数。然而,在大多数情况下,你并不需要声明block,你只需要在函数需要一个block参数是内联的实现block即可。下面的例子使用了qsort_b
函数。q_sort
跟标准的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到分派队列(dispatch queue)用来做多次调用。它需要三个参数:第一个指定迭代执行的次数;第二个指定了block被提交到那个分派队列(dispatch queue);第三个就是要提交的block,这个block只需要一个参数——当前迭代的index。
你可以使用繁琐的dispatch_apply
仅仅输出迭代的index:
#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);
});
将一个block用作方法的参数
Cocoa提供了多个使用block的方法,你可以传递一个block作为方法的参数。
在下面的例子中,若数组的前五个元素在给定过滤集合中出现,则输出其索引(index):
NSArray *array = @[@"A", @"B", @"C", @"A", @"B", @"Z", @"G", @"are", @"Q"];
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中包含一个特定的词(有一个局部变量指定),则将另一个局部变量设置为YES:
__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
拷贝block
通常,你不需要拷贝一个block。当你希望在定义block的范围被小伙之后还使用这个block是,你才会去拷贝一个block(只是你唯一需要拷贝一个block的地方)。
使用C函数来copy和release一个block:
Block_copy();
Block_release();
为了避免内存泄漏,你应该保持copy和release的平衡。
模式避免
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.
}
// ...
}
调试
你可以设置断点并且单步调试到block。你可以使用invoke-block
调用一个block:
$ invoke-block myBlock 10 20
如果你想要传递一个C字符串,你必须使用引号:
$ invoke-block doSomethingWithString "\"this string\""