Objective-C Block用法完全解读

block对象基于C语法和运行时特性,类似于标准C函数,但是除了提供可执行代码,还可以绑定栈和堆的内存变量。
你可以使用block来组合函数表达式,这些表达式可以传递给API,可选地存储,并由多个线程使用。block作为回调特别有用,因为block携带回调时执行的代码和执行期间所需的数据。
在GCC和Clang中支持block,它们随OS X v10.6 Xcode开发工具一起提供。
你可以在OS X v10.6及以后版本,以及iOS 4.0及以后版本中使用block。block运行时是开源的,由于Objective-C和c++都源自C, block被设计成可以与所有三种语言(以及objective – c++)兼容。
本文档帮助你了解什么是block对象,以及如何在C、C++或Objective-C中使用它们。
本文主要包括以下内容

  • 开始使用block,提供一个快速,实用的block。
  • block概述,提供对块的概念介绍。
  • 声明和创建block,向你展示如何声明block变量和如何实现block。
  • block和变量,描述block和变量之间的交互,并定义了__block存储类型修饰符。
  • 使用block,说明block的各种使用模式。
开始使用block下面几节将帮助你使用实际示例开始使用block。
声明和使用block
你可以使用^操作符来声明一个block变量,并指示一个block字面量的开头。block体本身包含在{}中,如下例所示(与C一样,分号; 表示语句的结尾):
int multiplier = 23; int (^myBlock)(int) = ^(int num){ return num * multiplier; }; int result = myBlock(48); NSLog(@"%d", result);

以上例子的解释如下图所示:
Objective-C Block用法完全解读

文章图片
注意,block中可以使用于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作为参数,一般用法是:对集合中的每个元素执行一个操作,或者执行某个操作后作为一个回调函数。下面的例子是在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中被修改。和上面的例子类似,你可以使用一个blick变量来计算有多少字符串被相等地比较,如下面的示例所示。为了便于说明,在本例中直接使用了blick,并使用currentLocale作为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概述?block对象为你提供了一种方法,可以在C语言和C派生语言(如Objective-C和C++)中创建专用函数体表达式。在其他语言和环境中,block块对象有时也称为“闭包”,在这里,它们通常被通俗地称为“block块”。
block功能
block是一个匿名的内联代码集合:
  • 具有与函数类似的类型化参数列表
  • 具有推断或声明的返回类型
  • 可以从定义状态的词法范围捕获状态
  • 可以修改词法作用域的状态
  • 可以与相同词法范围内定义的其他块共享修改的可能性
  • 在词法范围(堆栈框架)被销毁后,可以继续共享和修改词法范围(堆栈框架)中定义的状态
你可以复制一个块,甚至将它传递给其他线程以延迟执行(或者在它自己的线程中传递给runloop)。编译器和运行时安排从块中引用的所有变量在块的所有副本的生命周期中都被保留。虽然块对于纯C和c++是可用的,但块始终也是一个Objective-C对象。
block的用法
块表示通常较小的、自包含的代码段。因此,它们作为一种封装工作单元的方法特别有用,这些工作单元可以并发执行,或者在集合中的项上执行,或者作为另一个操作完成时的回调。
块是传统回调函数的一个有用的替代方法,主要有两个原因:
  • 它们允许你在调用点编写代码,然后在方法实现的上下文中执行这些代码。因此,块通常是框架方法的参数。
  • 它们允许访问本地变量。与使用需要包含执行操作所需的所有上下文信息的数据结构的回调不同,你只需直接访问本地变量。
?声明和创建block声明一个块引用
块变量保存对块的引用。声明它们的语法与声明指向函数的指针的语法类似,只是使用的是^而不是*。块类型与C类型系统的其余部分完全互操作。以下是所有有效的块变量声明:
void (^blockReturningVoidWithVoidArgument)(void); int (^blockReturningIntWithIntAndCharArguments)(int, char); void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);

块还支持可变参数(…),没有参数的块必须在参数列表中指定void。
块被设计成完全类型安全的,通过给编译器一组完整的元数据来验证块的使用、传递给块的参数和返回值的分配。可以将块引用转换为任意类型的指针,反之亦然。但是,你不能通过指针获取操作符(*)获取一个块——因此在编译时不能计算块的大小。
你也可以为块创建类型——这样做通常被认为是最佳实践,当你在多个地方使用一个给定的块类型:
typedef float (^MyBlockType)(float, float); MyBlockType myFirstBlock = // ... ; MyBlockType mySecondBlock = // ... ;

创建一个块block
可以使用^操作符来指示块字面量表达式的开头,它后面可能跟着一个包含在()中的参数列表,块体包含在{}中。下面的例子定义了一个简单的块,并将它赋值给一个先前声明的变量(oneFrom)——这里块后面跟一个简单的分号; ,表示语句结束。
float (^oneFrom)(float); oneFrom = ^(float aFloat) { float result = aFloat - 1.0; return result; };

如果没有显式声明块表达式的返回值,则可以从块的内容自动推断它。如果返回类型是推断出来的,而参数列表是void,那么你也可以忽略(void)参数列表。如果存在多个返回语句,它们必须完全匹配(必要时使用强制类型转换)。
全局块
在文件级别,你可以使用一个块作为全局字面量:
#import < stdio.h> int GlobalInt = 0; int (^getGlobalInt)(void) = ^{ return GlobalInt; };

block和变量 本文描述了块和变量之间的交互,包括内存管理。
变量的类型
在块对象的代码体中,可以用五种不同的方式处理变量。
你可以引用三种标准类型的变量,就像你可以从一个函数使用一样:
  • 全局变量,包括静态局部变量
  • 全局函数(在技术上不是变量)
  • 来自封闭范围的局部变量和参数
块还支持其他两种类型的变量:
  • 在函数级别上是__block的变量。这些变量在块(以及封闭的范围)中是可变的,如果任何引用块被复制到堆中,这些变量将被保留。
  • const导入。
最后,在方法实现中,块可以引用Objective-C实例变量。
以下规则适用于在一个块内使用变量:
  • 可以访问全局变量,包括包含在词法范围内的静态变量。
  • 传递给块的参数是可访问的(就像函数的参数一样)。
  • 栈(非静态)变量在封闭的词法范围内被捕获为const变量。
    • 它们的值是在程序中的块表达式处获取的。在嵌套块中,从最近的封闭范围捕获值。
  • 引用提供了与封闭的词法作用域局部相关的变量,这些变量是用__block存储修饰符声明的,因此是可变的。
    • 任何更改都反映在封闭的词法范围内,包括在相同的封闭词法范围内定义的任何其他块。这些将在__block存储类型中进行更详细的讨论。
  • 块的词法范围内声明的局部变量,其行为与函数中的局部变量完全相同。
    • 块的每次调用都提供该变量的新副本。这些变量可以作为const或引用变量使用在块内的块中。
下面的例子说明了局部非静态变量的使用:
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); };

要允许在一个块中更改一个变量,您可以使用_块存储类型修改器—请参阅_块存储类型。
块存储类型__block
你可以通过应用__block存储类型修饰符来指定导入的变量是可变的——也就是可读写的。块存储与本地变量的寄存器、自动存储和静态存储类型相似,但相互排斥。
__block变量存在于内存中,该内存在变量的词法范围与在该变量的词法范围内声明或创建的所有块和块副本之间共享。 因此,如果在栈内声明的块的任何副本在栈的末尾都可以生存(例如,通过排队到某个地方以供以后执行),则该内存将在栈帧的破坏中幸免。 给定词法范围内的多个块可以同时使用共享变量。
作为一种优化,块存储从栈开始—就像块本身一样。如果使用Block_copy复制块(或在Objective-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' }

对象和块变量
块作为变量提供了对Objective-C和c++对象以及其他块的支持。
Objective-C的对象
当一个块被复制时,它会创建对该块中使用的对象变量的强引用。如果你在一个方法的实现中使用一个块:
  • 如果通过引用访问实例变量,则强引用将指向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存储类型修饰符来标记它。
C++对象
一般来说,你可以在一个块中使用c++对象。在成员函数中,对成员变量和函数的引用通过隐式导入此指针,因此看起来是可变的。如果一个块被复制,有两个注意事项:
  • 如果你有一个用于基于栈的c++对象的__block存储类,那么通常使用的是copy构造函数。
  • 如果你在一个块中使用任何其他基于c++堆的对象,那么它必须有一个const copy构造函数。然后使用该构造函数复制c++对象。
块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

但是,我们经常将一个块作为参数传递给一个函数或方法。在这些情况下,通常创建一个“内联”块。
使用块作为函数参数
你可以将一个块作为函数参数传递,就像传递任何其他参数一样。但是,在很多情况下,不需要声明块;相反,你只需在需要它们作为参数的地方内联地实现它们。下面的示例使用了qsort_b函数。qsort_b类似于标准的qsort_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" }

注意,块包含在函数的参数列表中。
下一个示例展示如何使用dispatch_apply函数来使用一个块。dispatch_apply的声明如下:
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

函数为多个调用向调度队列提交一个块。它有三个参数:第一个指定要执行的迭代次数;第二个指定提交块的队列:第三个参数是块本身,它使用一个参数—迭代的当前索引。
你可以使用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提供了许多使用块的方法,你传递一个块作为方法参数,就像传递任何其他参数一样。
下面的示例确定出现在给定过滤器集中的数组中的前五个元素的索引。
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对象是否包含由局部变量指定的单词,并将另一个局部变量(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

复制块
通常,你不需要复制(或保留)块。当你希望在声明块的作用域被销毁后使用它时,你只需要复制一个。复制将一个块移动到堆中。
你可以复制和释放块使用C函数:
Block_copy(); Block_release();

为了避免内存泄漏,必须始终平衡使用Block_copy()和Block_release()。
避免的模式
一个块字面量(即,^{…})是表示块的栈本地数据结构的地址。因此,栈本地数据结构的范围是封闭的复合语句,所以应该避免以下示例中所示的模式:
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. } // ... }

调试
你可以将断点和单个步骤设置为块。你可以在GDB会话中使用invoke-block调用一个块,如下例所示:
$ invoke-block myBlock 10 20

【Objective-C Block用法完全解读】如果你想传递一个C字符串,你必须引用它。例如,要将此字符串传递到doSomethingWithString块,你需要编写以下代码:
$ invoke-block doSomethingWithString "\"this string\""

    推荐阅读