Objective-C Block

2,668 阅读11分钟

简介

Block 是一种在C, Objective-C, 和 C++ 上的语言特性, 允许我们创建独立的代码段. 对于这些代码段, 我们可以把它们像传递值一样在方法之间传递.

本文主要讲述Block的基本语法如何使用Block, 对于Block 更详细的介绍, 详见 Blocks Programming Topics.

Block 的基本语法

Block的定义

无参无返回值的Block

可以使用^符号定义一个Block :

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

我们使用{}限定Block的范围, 这一点与函数和方法的定义很相似.

类似于使用一个C函数指针, 我们可以声明一个变量来存储Block:

void (^simpleBlock)(void);

如果你不太熟悉C函数指针, 这个语法看起来可能会有点不寻常. 上面的例子中声明了一个叫做simpleBlock的变量, 它指向一个无参无返回值的Block. 我们可以为Block变量这样赋值:

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

需要注意的是: Block赋值和其它类型的变量赋值一样, 需要以;结尾.

我们也可以在声明变量的同时为其赋值:

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

之后我们就可以像这样调用Block了:

    simpleBlock();

Note: 如果我们调用了一个没有被赋值的Block变量, App会发生Crash.

带有参数和返回值的Block

和函数和方法类似, Block可以接收参数或者有返回值.

假如我们需要声明一个两个double类型相乘并返回结果的Block:

    double (^multiplyTwoValues)(double, double);

对应的Block实现如下:

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

在这个例子中, 返回值可以通过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 也有能力从临近作用域捕获上下文信息.

如果我们定义了一个方法内部的Block, Block可以捕获方法内作用域的上下文信息:

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

在这个例子中, anInteger在block外部声明, 但是在block定义时, 它的值被block"捕获"了.

这里捕获的仅仅是值, 当我们在block的定义和block的调用之间修改anInteger的值时:

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

testBlock()的输出并不会被anInteger的新值影响, 因为我们只捕获了值, 因此输出如下:

    Integer is: 42

这也意味着Block不能改变捕获变量的原值(甚至连捕获的值也不可以改变 - 由于捕获的值是作为const变量捕获的.).

__block的使用

如果我们需要从block内部改变外部捕获变量的值时, 我们可以在要修改的外部变量声明中使用__block 存储类型修饰符. 使用了__block修饰符的变量的存储空间, 被它本身的作用域与引用了它的block的作用域所共享:

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

由于使用了__block修饰anInteger, 因此anInteger的存储和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(Grand Central Dispatch)在子线程调用block, 或者在遍历集合时定义一个block重复调用它. 后文会详细描述这两种场景.

Block也可以用作回调(callbacks), 定义一段任务完成时执行的代码. 举例来说: App中可能从Web Service中请求信息, 由于这个请求可能花费较长的时间, 我们需要在请求过程中添加Loading, 在请求结束时隐藏Loading. 这个需求当然可以通过代理来实现: 创建一个合适的代理协议, 实现对应的方法, 设置任务代理对象, 当请求完成时调用代理方法. 如果我们使用Block, 由于我们在发出请求时就可以定义回调行为, 这使得同样的流程变得简单许多:

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

注意: 这个回调Block中为了调用hideProgressIndicator方法而捕获了self. 当捕获了self时, 我们需要格外小心, 因为很容易就可能引发循环引用. 后文对此我们会进行详细的描述.

从可读性的角度考虑, block使得任务发生时和任务完成时的代码同出一处, 而使用代理, 我们就不得不跟踪代理方法来确定方法的具体调用.

上面的beginTaskWithCallbackBlock:是如此声明的:

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

(void(^)void)指定了block参数的类型 -- 无参无返回值. 这个方法的实现和调用block相似:

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

含参的block作为方法参数:

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

在一个方法中, 最好只包含一个Block参数. 如果该方法中还需要其它非Block参数, 这个Block类型参数应当放置在参数列表的末尾:

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

这使得内联Block代码时, 方法的可读性会更好:

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

当我们需要定义多个block, 但是这些block拥有相同的签名(相同的参数列表+返回值类型), 我们可以为这个签名自定义一个类型:

    typedef void (^XYZSimpleBlock)(void);

这里定义了一个无参无返回值的Block类型XYZSimpleBlock.

我们可以使用它来作为方法参数类型或者创建Block变量:

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

自定义Block类型在处理返回Block类型的Block或者接收Block作为参数的Block时是很有用的, 比如:

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

这里complexBlock指向一个接收一个block作为参数(aBlock),并返回一个Block的Block. (有点绕 😆, 或许你有疑问, 为什么不是void(^)(void)(^complexBlock)(void(^)(void)).可以参考StackOverFlow的答案,里面描述了C解释器是如何一步步解释这个语法的). 使用自定义Block类型重写一下这个代码吧:

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

可读性立竿见影!

将Block作为对象的属性

如下代码定义了一个对象中的Block属性:

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

Note: 应当为Block指定copyattribute, 因为block需要被拷贝才能够在原作用域外存储它的上下文信息. 当我们使用ARC(Automatic Reference Counting)时, 我们不需要担心这些, 这些是默认使用的, 但是最好在属性attribute中显式表示. 更多信息详见: Blocks Programming Topics.

Block属性被设置和调用和Block变量相似:

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

也可以为属性使用类型定义:

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

避免Block中产生循环引用

当我们需要在Block中捕获self时, 比如我们定义了一个回调Block, 考虑内存管理是很重要的.

Block对于其捕获的对象都维持了强引用, 包括self. 这样就很容易产生强引用循环. 比如有一个对象, 它有一个用copy修饰的block属性, 而且这个block捕获了self:

@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
    }
}

这样, Block将不会强引用XYZBlockKeeper对象, 如果对象在block调用之前被销毁, weakSelf 将被置为nil.

Block的应用

使用Block简化遍历

除了回调之外, 很多Cocoa和Cocoa Touch API 使用Block简化任务, 比如集合的遍历. NSArray提供了一些和block相关的方法:

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

这个方法接受单个Block参数, 此Block在每次遍历时被调用:

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

Block本身接受三个参数, 前两个参数分别代表当前遍历的对象和该对象在数组中的下标, 第三个参数是一个BOOL类型的指针, 可以通过这个指针停止遍历:

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

我们也可以通过enumerateObjectsWithOptions:usingBlock:方法自定义遍历方式--比如指定NSEnumerationReverse,可以数组反向遍历元素.

如果在遍历Block中的代码是计算密集型的并且对于并发执行是安全的, 可以使用NSEnumerationConcurrent参数选项来在多线程执行block以提升性能:

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

注意: 当使用这个选项时遍历顺序是不确定的.

NSDictionary类也提供了Block相关的遍历方法:

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

与传统的遍历方法相比, 这种方式更为便捷.

使用Block简化并发任务

Block代表着一份独立的任务 -- 它包含了一段任务代码以及从上下文捕获的状态信息. 这使得它成为OS X/iOS异步编程中的理想工具. 使用Block定义任务并让系统处理这些任务, 从而不必使用线程之类的系统底层机制.

OS X 和 iOS 提供了各种各样的异步技术, 包含两种任务调度机制 -- 操作队列 (Operation Queues) 以及 GCD(Grand Central Dispatch). 这些机制围绕着任务与任务队列来实现. 我们可以将任务Block添加到队列中, 系统将会在处理器时间和资源可用时执行任务.

串行(Serial)队列只允许每次执行一个任务, 前一个任务完成之前, 下个任务不会开始. 并发(Concurrent)队列尽可能同时执行多个任务, 不必等待前面的任务完成.

在Operation Queue 中使用 Block Operation

我们可以通过创建NSOperationQueue队列, 并向其中添加NSOperation任务实例来完成任务.

我们可以创建自定义的NSOperation子类并自行实现复杂的任务功能, 也可以直接使用NSBlockOperation来创建Block任务:

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

虽然我们可以手动执行Operation对象, 但是更常用的场景是将其添加到OperationQueue中, 等待其执行:

// 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];

在使用Operation Queue时, 我们可以设置操作的优先级以及操作之间的依赖关系, 比如指定某个Operation必须在其它的若干个Operation完成后才可以开始执行. 我们也可以通过KVO监视任务的执行情况. 更多Operation相关, 详见Operation Queues.

在GCD(Grand Central Dispatch)中使用Block

GCD提供了Dispatch Queues 任务队列来管理任务使其同步或异步执行.

我们可以创建新的DispatchQueue或者使用GCD提供的DispatchQueue. 如果我们想要创建一个并发队列, 可以直接使用已有的队列 -- 通过dispatch_get_global_queue()返回, 并指定它的优先级:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

向队列中分发任务, 可以通过dispatch_async() 或者 dispatch_sync() 函数. dispatch_async() 函数会立即返回, 不会等待Block被触发.

dispatch_sync()函数会等到Block执行完成后才会返回.在需要异步执行的Block需要等到其它任务在主线程完成后继续执行时使用.

更多关于GCD的相关, 详见Dispatch Queues.

参考资料: Working with Blocks