个性化阅读
专注于IT技术分析

使用代码块block – Objective-C编程快速入门教程

上一章Objective-C编程快速入门教程请查看:值和集合

使用代码块Block

Objective-C类定义了一个将数据与相关行为相结合的对象。有时,只表示单个任务或行为单元而不是一组方法是有意义的。

块是添加到C、Objective-C和c++中的一种语言级别的特性,它允许你创建可以像传递值一样传递给方法或函数的不同代码段。block是Objective-C对象,这意味着它们可以被添加到像NSArray或NSDictionary这样的集合中。它们还能够从封闭范围捕获值,使其类似于其他编程语言中的闭包或lambdas。

本章解释了声明和引用块的语法,并展示了如何使用块来简化诸如集合枚举之类的常见任务。有关更多信息,请参见块编程主题。

块语法

定义块字面量的语法使用插入符号(^),如下所示:

    ^{
         NSLog(@"This is a block");
    }

与函数和方法定义一样,大括号表示块的开始和结束。在本例中,块不返回任何值,也不接受任何参数。

就像你可以使用一个函数指针来引用一个C函数一样,你可以声明一个变量来跟踪一个块,就像这样:

void (^simpleBlock)(void);

如果你不习惯使用C函数指针,那么它的语法可能有点不寻常。这个例子声明了一个名为simpleBlock的变量来引用一个没有参数且不返回值的块,这意味着这个变量可以被赋值给上面显示的块,如下所示:

    simpleBlock = ^{
        NSLog(@"This is a block");
    };

这就像任何其他变量赋值一样,因此语句必须在右括号后面用分号结束。你还可以组合变量声明和赋值:

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

一旦你已经声明和分配了一个块变量,你可以使用它来调用块:

simpleBlock();

注意:如果你试图使用未分配的变量(nil块变量)调用一个块,你的应用程序将崩溃。

块接受参数并返回值

与方法和函数一样,块也可以接受参数和返回值。

例如,考虑一个变量来引用一个返回两个值相乘结果的块:

double (^multiplyTwoValues)(double, double);

对应的块字面量可能是这样的:

    ^ (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

与任何函数定义一样,firstValue和secondValue用于引用调用块时提供的值。在本例中,返回类型是从块内的return语句中推断出来的。

如果你愿意,你可以通过在插入符号和参数列表之间指定返回类型来显式化:

    ^ double (double firstValue, double secondValue) {
        return firstValue * secondValue;
    }

一旦你声明和定义了block,你就可以像调用函数一样调用它:

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

块可以从封闭范围捕获值

除了包含可执行代码之外,块还能够从其封闭范围捕获状态。

例如,如果你在一个方法中声明一个block字面量,你可以捕获该方法范围内可访问的任何值,如下所示:

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

在本例中,在块外部声明一个整数,但是在定义块时捕获该值。

除非另外指定,否则只捕获值。这意味着如果你在定义block的时间和调用block的时间之间改变变量的外部值,就像这样:

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

块捕获的值不受影响。这意味着日志输出仍然显示:

Integer is: 42

它还意味着块不能改变原始变量的值,甚至不能改变捕获的值(它被捕获为const变量)。

使用__block变量共享存储

如果你需要能够从一个块中更改捕获的变量的值,你可以在原始变量声明上使用__block存储类型修饰符。这意味着变量位于原始变量的词法范围和该范围内声明的任何块之间共享的存储中。

举个例子,你可以这样重写前面的例子:

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

因为整数被声明为一个__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

可以将块作为参数传递给方法或函数

本章前面的每个示例都会在块定义之后立即调用它。在实践中,通常将块传递给在其他地方调用的函数或方法。例如,你可以使用Grand Central Dispatch在后台调用一个块,或者定义一个块来表示要重复调用的任务,例如在枚举集合时。本章后面将讨论并发性和枚举。

块也用于回调,定义任务完成时执行的代码。例如,你的应用程序可能需要通过创建一个执行复杂任务(如从web服务请求信息)的对象来响应用户操作。因为任务可能需要很长时间,所以应该在任务发生时显示某种进度指示器,然后在任务完成后隐藏该指示器。

可以使用委托/代理来完成此任务:你需要创建合适的委托协议,实现所需的方法,将对象设置为任务的委托,然后等待任务完成后调用对象上的委托方法。

块使这变得更容易,然而,因为你可以定义回调行为在你开始的任务,像这样:

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

本例调用一个方法来显示进度指示器,然后创建任务并告诉它开始。回调块指定任务完成后执行的代码;在本例中,它只是调用一个方法来隐藏进度指示器。注意,这个回调块捕获self,以便能够在调用时调用hideProgressIndicator方法。在捕获self时一定要小心,因为很容易创建强引用循环,如后面在捕获self时避免强引用循环中所述。

在代码可读性方面,块可以很容易地在一个地方看到任务完成之前和之后会发生什么,从而避免了通过委托方法来查找将要发生什么。

本例中显示的beginTaskWithCallbackBlock:方法的声明如下:

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

(void (^)(void))指定参数是一个不接受任何参数或返回任何值的块。该方法的实现可以以通常的方式调用block:

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

期望使用一个或多个参数的块的方法参数的指定方法与使用块变量的方法相同:

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

块应该总是方法的最后一个参数

最好的做法是只对一个方法使用一个块参数。如果该方法还需要其他非块参数,块应该放在最后:

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

这使得方法调用在内联指定块时更容易读取,就像这样:

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

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

如果需要定义多个具有相同签名的块,则可以为该签名定义自己的类型。

例如,你可以为一个没有参数或返回值的简单块定义一个类型,如下所示:

typedef void (^XYZSimpleBlock)(void);

然后你可以使用你的自定义类型的方法参数或创建块变量:

    XYZSimpleBlock anotherBlock = ^{
        ...
    };

- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
    ...
    callbackBlock();
}

在处理返回块或接受其他块作为参数的块时,自定义类型定义特别有用。考虑下面的例子:

void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
    ...
    return ^{
        ...
    };
};

complexBlock变量指的是将另一个块作为参数(aBlock)并返回另一个块的块。

重写代码以使用类型定义使其更具可读性:

XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
    ...
    return ^{
        ...
    };
};

对象使用属性来跟踪块

定义一个属性来跟踪一个块的语法类似于一个块变量:

@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end

注意:你应该指定copy作为property属性,因为需要复制一个块来跟踪它在原始范围之外捕获的状态。在使用自动引用计数时不需要担心这个问题,因为它是自动发生的,但是对于property属性来说,显示结果行为是最佳实践。有关更多信息,请参见程序设计主题。

块属性像其他块变量一样被设置或调用:

    self.blockProperty = ^{
        ...
    };
    self.blockProperty();

也可以对块属性声明使用类型定义,如下所示:

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

捕获self时避免强引用循环

如果你需要在一个块中捕获self,例如在定义回调块时,考虑内存管理的影响是很重要的。

块保持对任何捕获对象的强引用,包括self,这意味着很容易结束一个强引用循环,例如,一个对象维护一个捕获self的块的copy属性:

@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end

@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
        [self doSomething];    // capturing a strong reference to self
                               // creates a strong reference cycle
    };
}
...
@end

编译器会对类似这样的简单示例发出警告,但是更复杂的示例可能会涉及多个对象之间的强引用来创建循环,从而使诊断变得更加困难。

为了避免这个问题,最好的做法是捕获对self的弱引用,如下所示:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];   // capture the weak reference
                                  // to avoid the reference cycle
    }
}

通过捕获指向self的弱指针,块将不会与XYZBlockKeeper对象保持强关系。如果在调用块之前释放该对象,weakSelf指针将被简单地设置为nil。

块可以简化枚举

除了一般的完成处理程序之外,许多Cocoa和Cocoa Touch API还使用块来简化常见的任务,比如集合枚举。例如,NSArray类提供了三种基于块的方法,包括:

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

这个方法采用一个参数,它是一个块,在数组中的每个项目调用一次:

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

块本身有三个参数,前两个参数引用当前对象及其在数组中的索引。第三个参数是一个指向布尔变量的指针,你可以用它来停止枚举,就像这样:

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

还可以使用enumerateObjectsWithOptions:usingBlock:方法自定义枚举。例如,指定NSEnumerationReverse选项将以相反的顺序遍历集合。

如果枚举块中的代码是处理器密集型的,并且对于并发执行是安全的,那么可以使用NSEnumerationConcurrent选项:

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

此标志指示枚举块调用可能分布在多个线程中,如果块代码特别需要大量处理器资源,则可以提供潜在的性能提升。注意,使用此选项时枚举顺序未定义。

NSDictionary类还提供了基于块的方法,包括:

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

这使得枚举每个键值对比使用传统循环更加方便。

块可以简化并发任务

块表示不同的工作单元,将可执行代码与从周围范围捕获的可选状态相结合。这使得它非常适合使用OS X和iOS可用的并发选项之一进行异步调用。不必考虑如何使用线程之类的低级机制,只需使用块定义任务,然后让系统在可用处理器资源时执行这些任务即可。

OS X和iOS提供了多种并发技术,包括两种任务调度机制:操作队列和中央调度。这些机制围绕着等待调用的任务队列的思想。你可以按照需要调用块的顺序将它们添加到队列中,当处理器时间和资源可用时,系统会将它们从队列中取出以供调用。

串行队列只允许一次执行一个任务——在前一个任务完成之前,不会将队列中的下一个任务从队列中取出并调用。并发队列调用尽可能多的任务,而不需要等待以前的任务完成。

对操作队列使用块操作

操作队列是任务调度的Cocoa和Cocoa Touch方法。你创建一个NSOperation实例来封装一个工作单元以及任何必要的数据,然后将该操作添加到一个NSOperationQueue以执行。

虽然你可以创建自己的自定义NSOperation子类来实现复杂的任务,也可以使用NSBlockOperation来创建一个使用block的操作,就像这样:

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    ...
}];

可以手动执行操作,但操作通常添加到现有的操作队列或你自己创建的队列中,准备执行:

// schedule task on main queue:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
 
// schedule task on background queue:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];

如果使用操作队列,可以配置操作之间的优先级或依赖关系,例如指定在一组其他操作完成之前不应执行某个操作。你还可以通过KVO来监视操作状态的变化,这使得更新进度指示器(例如,当任务完成时)变得很容易。

有关操作和操作队列的更多信息,请参见操作队列。

使用Grand Central Dispatch在调度队列上调度块

如果需要调度任意代码块执行,可以直接使用Grand Central dispatch (GCD)控制的调度队列。调度队列使得相对于调用方更容易同步或异步地执行任务,并以先入先出的顺序执行它们的任务。

你可以创建自己的调度队列,也可以使用GCD自动提供的一个队列。例如,如果需要为并发执行调度一个任务,可以使用dispatch_get_global_queue()函数获得对现有队列的引用,并指定队列优先级,如下所示:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

要将块分派到队列,可以使用dispatch_async()或dispatch_sync()函数。dispatch_async()函数立即返回,无需等待调用块:

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

dispatch_sync()函数直到块完成执行才返回;例如,你可以在一个并发块需要在主线程上等待另一个任务完成后才能继续执行的情况下使用它。

有关调度队列和GCD的更多信息,请参见调度队列。

赞(4)
未经允许不得转载:srcmini » 使用代码块block – Objective-C编程快速入门教程

评论 抢沙发

评论前必须登录!