Block的使用

Working with Blocks

在编程领域里,一个牛逼程序员和一个二逼程序员之间的区别主要是其对所用编程语言优秀特性的运用方式。要说到Objective-C语言时,那么通常开发者和大牛的区别可能就是对Block书写代码的运用能力了。html

Block编程并非Objective-C语言首创的一个编程方式,Block也同时也以其余的命名方式存在于其余的编程语言中,例如在Javascript中闭包;Block首次于iOS 4.0版本中引入,其后便被普遍地接受和运用。在随后的iOS版本中,为了适用Block,Apple重写了不少的framework方法。彷佛Block在必定程度上已经成为了将来的一种编程方式。可是Block究竟是什么呢?程序员

Block是什么

Block是一种添加到C、Objective-C和C++语言中的一个语言层面的特性,它容许您建立不一样的代码段,并像值同样的传递到方法或函数中。Block是一个Objective-C对象,这就意味着其能够被保存在NSArray或者NSDictionary中,Block还可以在本身的封闭做用域中截获到值(即所谓的变量截获),Block其实和其余编程语言中的closure(闭包)或lambda是很相似的。编程

Block 语法

blocks.jpg

在定义Block的语法中咱们使用**脱字符(^)**来标识这是一个Block,以下所示:数组

^{
         NSLog(@"This is a block");
    }
复制代码

与函数和方法定义同样,大括号同时也表明着Block的开始与结束。 在这个例子中,Block不返回任何值,而且不接受任何参数。安全

与经过使用函数指针来引用C函数的相似方式,你也能够经过声明一个变量来记录Block,如:bash

void (^simpleBlock)(void);
复制代码

若是你对处理C语言的函数指针不熟悉,那么上面的这种语法看起来会有点让人摸不着头脑。 上面的例子中声明了一个名字为simpleBlock的变量,用以引用一个没有参数也没有返回值的Block,这意味着这个Block变量能够被最上面的Block所赋值,以下所示:多线程

simpleBlock = ^{
        NSLog(@"This is a block");
    };
复制代码

这和任何其余变量赋值同样,因此语法上必须以大括号后面的分号做为结束。 您也能够将Block变量的声明和赋值组合起来:闭包

void (^simpleBlock)(void) = ^{
        NSLog(@"This is a block");
    };
复制代码

一旦Block被声明且赋值后,您就能够调用Block了,调用方法以下:并发

simpleBlock();
复制代码

注意:若是你试图调用一个没有被赋值过的Block变量,你的应用会崩溃的。app

Block的参数和返回值

像方法和函数同样,Block即接受参数也有返回值;例如,一个返回两个值乘积的Block变量:

double (^multiplyTwoValues)(double, double);
复制代码

对应于上面的Block变量,其相应的Block应该是这样的:

^ (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }
复制代码

firstValue和secondValue用于引用在调用Block时提供的值,就像任何函数定义同样。 在此示例中,返回类型是从Block内的return语句推断的。

若是你喜欢,你能够经过在脱字符(^)和参数列表之间指定来使返回类型显式地写出:

^ double (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }
复制代码

一旦你声明和定义了Block,你就能够像调用函数那样调用Block:

double (^multiplyTwoValues)(double, double) =
                              ^(double firstValue, double secondValue) {
                                  return firstValue * secondValue;
                              };
 
    double result = multiplyTwoValues(2,4);
 
    NSLog(@"The result is %f", result);
复制代码

Block能够截获外部变量

除了包含可执行代码以外,Block还具备从其封闭的做用域内截获变量状态的能力。

例如,若是在方法中声明一个Block,它能够截获该方法做用域内可访问的任何变量的值,以下所示:

- (void)testMethod {
    int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    testBlock();
}
复制代码

在此示例中,anInteger是在Block以外声明的一个变量,可是Block却在定义时截获了变量的值。

int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    anInteger = 84;
 
    testBlock();
复制代码

Block截获的变量的值没有改变。 这意味着日志的输出将显示为:

Integer is: 42

这也意味着Block不能改变原始变量的值,甚至是截获变量值(被截获的变量变成了一个常量)。

__block修饰的变量

当一个Block被复制后(当Block截获到外部变量时,Block就会被复制到堆上),__block声明的栈变量的引用也会被复制到了堆里,复制完成以后,不管是栈上的Block仍是刚刚产生在堆上的Block(栈上Block的副本)都会引用该变量在堆上的副本。

你能够像下面这样重写当前的例子:

__block int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
    };
 
    anInteger = 84;
 
    testBlock();
复制代码

由于变量anInteger被声明为一个__block变量,它的内存地址与声明中Block的变量地址是共享的。 这意味着日志输出如今将显示:

Integer is: 84

这同时也标志着Block能够修改其变量的原始值,以下所示:

__block int anInteger = 42;
 
    void (^testBlock)(void) = ^{
        NSLog(@"Integer is: %i", anInteger);
        anInteger = 100;
    };
 
    testBlock();
    NSLog(@"Value of original variable is now: %i", anInteger);
复制代码

此次的输出会是:

Integer is: 42

Value of original variable is now: 100

Block做为方法或函数的参数

前面的每一个例子都是在定义以后会当即调用Block。 在平常代码编写中,一般将Block做为参数传递给函数或方法以在其余地方进行调用。 例如,您可使用GCD在后台调用Block,或者定义一个要重复调用任务的Block,例如枚举集合时。 并发和枚举将在后面讨论。

Block也用于回调,即定义任务完成时要执行的代码。 例如,您的应用程序可能须要经过建立执行复杂任务的对象(例如从Web服务请求信息)来响应用户操做。 由于任务可能须要很长时间,您应该在任务发生时显示某种进度指示器(菊花),而后在任务完成后隐藏该指示器(菊花)。

固然,你可使用委托来完成这个任务:你须要建立一个合适的委托协议,实现所需的方法,将你的对象设置为任务的委托,而后等待,一旦任务完成时它在你的对象上调用一个委托方法。

然而,Block可让这些更加容易,由于您能够在启动任务时定义回调行为,以下所示:

- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];
 
    XYZWebTask *task = ...
 
    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}
复制代码

此示例调用一个方法来显示进度指示器(菊花),而后建立任务并指示它开始。 回调Block指定任务完成后要执行的代码; 在这种状况下,它只是调用一个方法来隐藏进度指示器(菊花)。 注意,这个回调block截获了self,以便可以在调用时调用hideProgressIndicator方法。 在截获self时要当心,由于它很容易建立一个strong类型的循环引用,详情见后面的如何在block截获了self后避免循环引用

在代码可读性方面,该Block使得在一个位置上很容易看到在任务完成以前和完成以后会发生哪些状况,从而避免须要经过委托方法来查找将要发生的事情。

此示例中显示的beginTaskWithCallbackBlock:方法的声明以下所示:

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;

复制代码

(void(^)(void))上一个没有参数没有返回值的Block。 该方法的实现能够以一般的方式调用Block:

- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
    ...
    callbackBlock();
}

复制代码

Block做为方法的参数,其所拥有的多个或一个参数在形式上应与单纯的Block变量相同:

- (void)doSomethingWithBlock:(void (^)(double, double))block {
    ...
    block(21.0, 2.0);
}

复制代码

Block应该始终做为方法的最后一个参数

若是方法中含有Block以及其余非Block的参数, 那么Block参数应该始终做为方法的最后一个参数写出,如:

- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;

复制代码

这使得在指定Block内联时更容易读取方法的调用,以下所示:

[self beginTaskWithName:@"MyTask" completion:^{
        NSLog(@"The task is complete");
    }];
复制代码

使用类型定义来简化Block语法

若是须要使用相同的Block类型来定义多个Block,您可能须要为该类型进行从新的定义。 例如,您能够为没有参数没有返回值的简单Block定义类型(即为Block类型取一个别名):

typedef void (^XYZSimpleBlock)(void);
复制代码

而后,可使用自定义类型的Block做为方法的参数或用自定义类型来建立Block变量:

XYZSimpleBlock anotherBlock = ^{
        ...
    };
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    ...
    callbackBlock();
}
复制代码

自定义类型定义在处理做为返回值的Block或将其余Block用做参数的Block时特别有用。 请看如下示例:

void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
    ...
    return ^{
        ...
    };
};
复制代码

complexBlock变量指的是将另外一个Block做为参数(aBlock)并返回另外一个Block的Block。 使用类型定义来重写上面的代码,这使的这段代码更加可读:

XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
    ...
    return ^{
        ...
    };
};
复制代码

对象使用Block做为属性

定义一个Block属性的语法相似于声明一个Block变量:

@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
复制代码

注意:您应该将copy指定为属性修饰符,变量被Block截获后,会改变自身在内存的位置,由栈区变为堆区,因此Block也须要将本身复制到堆区,以应对这种改变。 当使用自动引用计数时,你是不须要担忧的,由于它会自动发生的,可是属性修饰符的最佳作法是显示结果行为。 有关更多信息,请参阅Blocks Programming Topics

Block属性的设置及调用和其余的Block变量是同样的:

self.blockProperty = ^{
        ...
    };
    self.blockProperty();
复制代码

同时也可使用类型定义的方式声明一个Block属性,以下:

typedef void (^XYZSimpleBlock)(void);
 
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
复制代码

如何在Block截获了self后避免循环引用

若是在定义一个Block回调时,须要在Block中截获self,内存管理的问题是须要引发重视的。

Block对任何截获的对象都是强引用,包括self;记住这一点后,想要解开循环引用就不是很难了,以下,一个拥有Block属性的对象,在Block内截获了self

@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
        [self doSomething];    // Block对self是强引用的
                               // 这就产生了循环引用
    };
}
...
@end
复制代码

像上面这样的一个简单例子中,编译器是会在你编写代码时报警告的;可是对于有多个强应用对象在一块儿产生的循环引用问题,编译器是很难发现循环引用问题的:

为了不出现这种问题,最好的方式是截获一个弱引用的self,以下所示:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self; 
      
    //或__weak typeof(self) weakSelf = self;
    
    self.block = ^{
        [weakSelf doSomething];   // 截获一个弱引用self
                                  // 以此来避免循环引用   
    }
}
复制代码

经过在Block内截获了一个弱指针指向的self,这样Block就不会再维持对XYZBlockKeeper对象的强引用关系了。若是对象在Block被调用以前释放了,指针weakSelf就会被置为空;

Block能够用来简化枚举(enumerate)

除了做为基本的回调使用外,许多的Cocoa 和 Cocoa Touch 框架的API也用Block来简化任务,如集合枚举。例如,NSArray就提供了三个含有Block的方法:

- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;

复制代码

这个方法接受一个Block的参数,这个参数对于数组中的每一个项目调用一次:

NSArray *array = ...
    [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        NSLog(@"Object at index %lu is %@", idx, obj);
    }];
复制代码

上面的Block须要三个参数,前两个参数指向当前对象及其在数组中的索引。 第三个参数是一个指向布尔变量的指针,能够用来中止枚举,以下所示:

[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        if (...) {
            *stop = YES;
        }
    }];  
复制代码

还可使用enumerateObjectsWithOptions:usingBlock:方法自定义枚举。 例如,指定NSEnumerationReverse这一选项将会反向遍历集合。

若是枚举Block中的代码是处理器密集型(processor-intensive)而且是安全的并发执行 -- 您可使用NSEnumerationConcurrent选项:

[array enumerateObjectsWithOptions:NSEnumerationConcurrent
                            usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
        ...
    }];
    
复制代码

这个flag指示Block枚举的调用可能会是多线程分布的,若是Block代码是专门针对处理器密集型的,那么这样作对性能会有潜在的提高。注意,当使用这个选项时,这个枚举的顺序是未定义的。

NSDictionary同时也提供一些基于Block的方法,以下所示:

NSDictionary *dictionary = ...
    [dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
        NSLog(@"key: %@, value: %@", key, obj);
    }];
复制代码

如上面的例子所示:相比使用传统的循环遍历,使用枚举键值对的方式会更加方便,

Block能够用来简化并发任务

每一个Block表明一个不一样的工做单元,就是可执行代码与Block周围做用域中截获的可选状态组合。 这使的Block成为OS X和iOS中理想的异步并发调用可选项之一。 且无需弄清楚如何使用线程等低级机制,您可使用Block定义任务,而后让系统在处理器资源可用时执行这些任务。

OS X和iOS提供了多种并发技术,包括两种任务调度机制:Operation queues和GCD。 这些机制围绕着一个等待被调用的任务队列而设。 您按照须要调用它们的顺序将Block添加到这一队列中,当处理器时间和资源可用时,系统将对这一队列中的Block进行调用。

串行队列只容许一次执行一个任务 -- 队列中的下一个任务直到前一个任务完成才会被调用,在此期间这一任务将不会离开队列。 并发队列会调用尽量多的任务,而没必要等待前面的任务完成。

使用Block操做队列

操做队列是Cocoa和Cocoa Touch框架的任务调度方式。 您建立一个NSOperation实例来封装一个工做单元以及任何须要的数据,而后将该操做添加到NSOperationQueue中来执行。

虽然您能够建立本身的自定义NSOperation子类来实现复杂的任务,但也能够经过NSBlockOperation使用Block的方式建立一个操做,以下所示:

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
}];
复制代码

您能够手动执行操做,但操做一般添加到现有的操做队列或您本身建立的队列中去执行:

// 在主队列执行任务:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
 
// 在后台队列执行任务:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

复制代码

若是使用操做队列,能够配置操做之间的优先级或依赖关系,例如指定一个操做先不执行,直到一组其余操做完成才执行。例如,您还能够经过KVO的方式监听操做状态的改变,而后在任务完成时,更新进度指示器(菊花):

更多关于操做和队列操做的信息,见Operation Queues

使用GCD在调度队列中给Block进行进度安排。

若是须要安排任意Block代码执行的话,您能够直接使用由Grand Central Dispatch(GCD)控制的调度队列(dispatch queues)。 调度队列使得相对于调用者同步或异步地执行任务变得容易,而且以先进先出的顺序执行它们的任务。

您能够建立本身的调度队列(dispatch queue)或使用GCD自动提供的队列。 例如,若是须要安排并发执行的任务,能够经过使用dispatch_get_global_queue()函数并指定队列优先级来获取对现有队列的引用,以下所示:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

//要将该block分派到队列中,您可使用dispatch_async()或dispatch_sync()函数。
// dispatch_async()函数不会等待要调用的block执行完毕,而是当即返回:

dispatch_async(queue, ^{
    NSLog(@"Block for asynchronous execution");
});
复制代码

更多关于队列调度和GCD的问题见Dispatch Queues.

文章主要翻译自Apple官方文档Working with Blocks

喜欢的话,给个关注,谢谢!!

相关文章
相关标签/搜索