iOS Block详解3
——译自Apple Reference Library《Blocks Programming Topic》
简介
块对象是C语言的句法和运行时特性。它类似于标准C函数,但可以将代码、变量绑定到堆(heap)、栈(stack)。一个块还维护了一系列的状态,这些状态或数据影响着执行的结果。
可以把块组成函数表达式,用于传递给API,或者使用在多线程里。最有用的是回调,因为块在回调时能把代码和数据一起传送。
在OSX 10.6的Xcode中,可以使用块,它随GCC和 Clang 一起集成。在OSX 10.6及iOS 4.0以后支持块语法。 块运行时是开源的,它能被集成到 LLVM’s compiler-rt subproject repository 中。标准C工作组的 N1370: Apple’s Extensions to C 中 ( 其中也包括垃圾回收 ) 对块进行了定义。O-C和C++都来自于C,块在3种语言(包括O-C++)都能工作。
这篇文档中,你会学习到什么是块对象,以及怎样在C,C++和O-C中使用它,使代码的性能和可维护性更高。
开始
声明块
^ 操作符声明一个块变量的开始(跟C一样用; 来表示表达式结束),如代码所示:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
解释 :
注意,块可以使用同一作用域内定义的变量。
一旦声明了块,你可以象使用函数一样调用它:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
printf("%d", myBlock(3));
直接使用块
很多情况下,你不必声明块变量,而简单地写一个行内块并把它当作一个参数,如下面的代码所示。
gsort_b类似标准的 gsort_r 函数,但它最后一个参数是一个块。
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 和块
在Cocoa框架中,有几种把块作为参数的方法。典型的是在集合中进行一个操作,或者在操作完成后作为一个回调。下列代码显示如何在NSArray的sortedArrayUsingComparator方法中使用块。这个方法使用了一个块参数。为了演示,在这里把块定义为一个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"
)*/
块变量
块的一个强大功能它可以改变在同一作用域内的变量。用__block修饰符来标识一个变量能够被块改变。使用下面的代码,你可以用一个块变量计算进行比较的字符串中有多少是相同的。为了演示,块是直接使用的,同时currentLocal变量对于块来说是只读的。
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",
"Str/U00eeng 21",
"Stri/U00f1g 21"
)
orderedSameCount: 2
*/
相关概念
块提供了一种方法,允许你创建一种特殊的函数体,在C及C派生语言如O-C和C++中,可以把块视为表达式。其他语言中,为了不与C术语中的块混淆,块也被称作closure(国内译作闭包),这里它们都称做blocks。
块的功能
块是行内的代码集合:
▪ 同函数一样,有类型化参数列表
▪ 有返回结果或者要申明返回类型
▪ 能获取同一作用域(定义块的相同作用域)内的状态
▪ 可以修改同一作用域的状态(变量)
▪ 与同一范围内的其他块同享变量
▪ 在作用域释放后能继续共享和改变同一范围内的变量
甚至可以复制块并传递到其他后续执行的线程。编译器和运行时负责把所有块引用的变量保护在所有块的拷贝的生命周期内。对于C和C++,块是变量,但对于O-C ,块仍然是对象。
块的使用
块通常代表小段的、自包含的代码片段。
因此,它们封装为可以并行执行的工作单元额外有用,要么用于在集合中进行遍历,要么在其他操作完成使作为回调。
块代替传统回调函数的意义有两个:
1. 它们允许在方法实现的调用中就近地写入代码。而且块经常被作为框架中一些方法的参数。
2. 它们允许访问本地变量。在进行线程操作时,相比回调函数需要把所需的上下文信息植入数据结构中而言,块直接访问本地变量显然更加简单。
块的声明和创建
声明块变量
块变量引用了块。它的声明语法类似函数指针,除了需要使用^代替*。
void (^blockReturningVoidWithVoidArgument)(void);
int (^blockReturningIntWithIntAndCharArguments)(int, char);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);
块支持可变参数(…)。如果块没有参数,则必需使用void来代替整个参数列表。
块是类型安全的,通过设置编译选项,编译器会检查块的调用、参数和返回类型。可以把块变量转换为指针类型,但不能使用*对其解除引用——块的长度在编译时无法确定。
可以创建一个块类型,这样你就可以把块当作一个可以反复多次使用的符号:
typedef float (^MyBlockType)(float, float);
MyBlockType myFirstBlock = // ... ;
MyBlockType mySecondBlock = // ... ;
创建块
块以^开始,以;结束。下面显示了块的定义:
int (^oneFrom)(int);
oneFrom = ^(int anInt) {
return anInt - 1;
};
如果未显式地声明块的返回值类型,可能会自动从块代码中推断返回类型。如果参数列表为void,而且返回类型依靠推断,你可以省略参数列表的void。否则,当块中存在return语句时,它们应当是精确匹配的(可能需要必要的类型转换)。
全局块
可以把块定义为全局变量,在文件级别上使用。
#import <stdio.h>
int GlobalInt = 0;
int (^getGlobalInt)(void) = ^{ return GlobalInt; };
块和变量
本节描述块和变量之间的交互,包括内存管理。
变量类型
在块代码内部,变量会被处理为5种不同情况。
就像函数一样,可以引用3种标准的变量:
▪ 全局变量,包括静态变量
▪ 全局函数
▪ 本地变量及参数(在块范围内)
此外块还支持两种变量:
1. 在函数级别,是__block变量。它们在块范围内是可变的,如果所引用的块被复制到堆后,它们也是被保护的。
2. const imports.
在方法体内,块还可以引用O-C 实例变量,见 “ 对象和块变量 ”.
在块中使用变量有以下规则:
1. 可访问在同一范围内的全局变量包括静态变量。
2. 可以访问传递给块的参数(如同函数参数)。
3. 同一范围的栈(非static)变量视作const变量。它们的值类似块表达式。嵌套块时,从最近的作用域取值。
4. 在同一范围内声明的变量,如果有__block修饰符修饰,则值是可变的。在该范围内包括同一范围内的其他块对该变量的改变,都将影响该作用域。具体见“__block 存储类型”。
5. 在块的范围内(块体)声明的本地变量,类似于函数中的本地变量。块的每次调用都会导致重新拷贝这些变量。这些变量可作为const或参考(by-reference)变量。
下面演示本地非静态变量的使用:
int x = 123;
void (^printXAndY)(int) = ^(int y) {
printf("%d %d/n", x, y);
};
printXAndY(456); // prints: 123 456
注意,试图向x进行赋值将导致错误:
int x = 123;
void (^printXAndY)(int) = ^(int y) {
x = x + y; // error
printf("%d %d/n", x, y);
};
要想在块内改变x的值,需要使用__block修饰x。见“__block存储类型”。
__block 存储类型
你可以规定一个外部的变量是否可变——可读写——通过使用__block存储类型修饰符。__block存储类似但不同于register,auto和static存储类型。
__block变量在变量声明的作用域、所有同一作用域内的块,以及块拷贝之间同享存储。而且这个存储将在栈帧(stack frame)释放时得以保留,只要同一帧内申明的块的拷贝仍然存活(例如,被入栈以便再次使用)。在指定作用域内的多个块能同时使用共享变量。
作为一种优化,块存储使用栈存储,就如同块自身一样。如果使用Block_copy拷贝块(或者在O-C向块发送copy消息),变量被拷贝到堆里。而且,__block变量的地址随后就会改变。
__block变量有两个限制:不能是可变长度的数组,也不能是包含C99可变长度数组的结构体。
下面显示了__block变量的使用:
__block int 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
下面显示了在块中使用多种类型的变量:
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'
}
对象和块变量
块提供了对O-C和C++对象的支持 。
O-C对象
在引用计数的情况下,当你在块中引用一个O-C对象,对象会被retained。甚至只是简单引用这个对象的实例变量,也是一样的。
但对于__block标记的对象变量,就不一样了。
注意:在垃圾回收的情况下,如果同时用__weak和__block修饰变量,块可能不一定保证它是 可用 的。
如果在方法体中使用块,对象实例变量的内存管理规则 比较微妙:
▪ 如果通过对象引用方式访问实例变量,self 被 retained;
▪ 如果通过值引用方式访问实例变量,变量是retained;
下面代码演示了这2种情况:
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);
});
C++ 对象
一般,可以在块中使用C++对象。在成员函数中对成员变量进行引用,俨然是对指针的引用,可以对其进行改变。如果块被拷贝,有两种结果:
如果有__block存储类型的类,该类是基于栈的C++对象,通常会使用复制构造函数;
如果使用了其他块中的基于栈的C++对象,它必需有一个const的复制构造函数。该C++对象使用该构造函数进行拷贝。
块
拷贝块时,其引用的其它块可能也被拷贝(从顶部开始)。如果有块变量,并且在这个块中引用了一个块,那个块也会被拷贝。
拷贝一个基于栈的块时,你得到的是新的块。拷贝一个基于堆的块时,只是简单的增加了retain数,然后把copy方法/函数的结果返回这个块。
使用块
块的调用
如果把块申明为变量,可以把它当成函数使用,例如:
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
但时常会将块以参数形式传递给一个函数或方法,这样,就会使用行内(inline)块。
把块作为函数参数
在这种情况下,不需要块申明。简单地在需要把它作为参数的地方实现它就行。如下所示,gsort_b是一个类似标准gsort_r的函数,它的最后一个参数使用了块。
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" }
注意,块包含在函数的参数列表中。
接下来的例子显示如何在dispath_apply函数中使用块。dispatch_apply的声明是:
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));
这个函数把块提交给dispatch队列以进行调用。它有3个参数:要操作的次数;块被提交到的队列;块——这个块有一个参数——遍历操作的当前次数。
可以用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);
});
把块作为参数使用
Cocoa提供了大量使用块的方法。把块作为参数使用与使用其他类型的参数并无不同。
以下代码判断数组中前5个元素中含有给定filter集合的索引。
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 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
块复制
一般,你不需要复制块。只有当你希望在这个块申明的范围外使用它时需要复制它。复制将导致块移动到堆中。
可以使用C函数释放和复制块。
Block_copy();
Block_release();
对于O-C,则可向块发送copy,retain和release(以及autorelease)消息。
为避免内存泄露,一个Block_copy()总是对应一个Block_release()。每个copy/retain总是有对应的release(或autorelease)——使用垃圾回收则例外。
避免的用法
一个块声明(即^{…})是一个本地栈式数据结构(stack-local data structure)的地址,这个地址就代表了块。本地栈式数据结构是{}围住的复合语句,因此应该避免如下用法:
void dontDoThis() {
void (^blockArray[3])(void);// 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
}
// ...
}
调试
可以在块内设置断点,并进行单步调试。在GDB会话中,使用invoke-block调用块,比如:
$ invoke-block myBlock 10 20
如果需要传递C字符串,必需用双引号把它引住。例如,向doSomethignWithString块传递一个字符串:
$ invoke-block doSomethingWithString "/"this string/""