iOS开发之Block详解

Block是C, Objective-C和C++引入的一种语言特性, 它允许你定义一块单独的代码, 这块代码可以被当做方法和函数的参数来传递, 就好像它们是一个数值一样. Block是一个Objective-C对象, 这意味着它们可以被加入到集合中, 比如NSArray和NSDictionary. 它们还能从周围的环境中捕获数值, 类似于其他编程语言中的closures或者lambdas.

这篇文章主要讲解声明和引用Block的语法, 以及如何使用Block来完成一些简单的任务, 比如遍历集合.

Block语法

用^符号来定义一个block字面值:

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

与方法和函数类似, 大括号标志着block的开始和结束. 在上面的例子中, block没有返回值, 也没有参数.

就像用函数指针来引用一个C函数一样, 你也可以声明一个变量来引用一个block:

void (^simpleBlock)(void);

如果你没有接触过C语言的函数指针, 上面的语法看起来可能会有些陌生. 上面的例子声明了一个叫做simpleBlock的变量来引用一个block, 这个block没有参数, 并且也没有返回值, 这就意味着可以用它来引用上面的block字面值:

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

这就和其他的赋值语句一样, 所以在语句末尾必须加分号. 也可以把变量声明和赋值结合起来:

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

一旦声明了一个block变量, 并且进行了赋值, 你就可以用它来调用block了:

simpleBlock(); // 输出This is a block.

注意, 如果你试图调用一个未赋值的block变量(block为nil), 你的APP就会崩溃.

Block可以有参数和返回值

就像方法和函数一样, block也可以有参数和返回值. 举个例子, 考虑一个block变量, 它引用的block用来返回两个数相乘的结果:

double (^multiplyTwoValues)(double, double);

对应的block字面值可能看起来像下面这样:

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

在调用block时, firstValue和secondValue用来引用所传入的参数, 就像函数一样. 在例子中, 返回值可以从block内部的返回语句中推断出来. 如果你喜欢, 也可以明确地指定返回值类型, 在^符号和参数列表之间:

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

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

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

Block可以从周围的环境中捕获数值

不仅能包含可执行的代码块, block也可以从它周围的环境中捕获变量的状态. 如果你在一个方法内部定义了一个block, 这个block就可以捕获所有它可以访问到的数值, 像这样:

- (void)testMethod {
    int anInt = 42;

    void (^testBlock)(void) = ^{
        NSLog(@"Int is %d.", anInt);
    };

    testBlock();
}

在例子中, anInt是在block外面定义的, 但是在定义block时就可以捕获到它的值.

需要注意的是, block捕获到的仅仅是变量当时的值(或状态), 这意味着如果你在定义block之后又修改了变量的值, 那么block内部捕获到的值并不会受到影响:

- (void)testMethod {
    int anInt = 42;

    void (^testBlock)(void) = ^{
        NSLog(@"Int is %d.", anInt);
    };

    anInt = 84; // 改变anInt的值

    testBlock(); // 仍然输出Int is 42. 
}

同时, 在block内部也不能修改它所捕获到的变量(变量其实是作为常量被捕获的), 如下面的例子:

- (void)testMethod {
    int anInt = 42;

    void (^testBlock)(void) = ^{
        anInt = 84; // 此行报错
        NSLog(@"Int is %d.", anInt);
    };

    testBlock();
}

__block关键字

如果你确实需要在block内部修改一个在外部定义的变量的值, 那么可以在定义变量时加上__block关键字. 这意味着变量所在的存储区域可以被它所在的作用域内的所有block共享.

我们重写之前的例子:

- (void)testMethod {
    __block int anInt = 42;

    void (^testBlock)(void) = ^{
        NSLog(@"Int is %d.", anInt);
    };

    anInt = 84;

    testBlock(); // 输出Int is 84.
}

这也意味着在block内部可以修改变量的值:

- (void)testMethod {
    __block int anInt = 42;

    void (^testBlock)(void) = ^{
        NSLog(@"Int is %d.", anInt);
        anInt = 84;
    };

    testBlock();
    NSLog(@"Int is now %d.", anInt);
}

输出结果:

2017-07-03 14:18:00.412 Blog[1456:126163] Int is 42.
2017-07-03 14:18:00.412 Blog[1456:126163] Int is now 84.

Block作为方法或函数的参数

在之前的例子中, 我们都是在block定义之后就立即调用它. 而在实际的开发中, block经常被作为方法或函数的参数在别处被调用. 我们可能会用GCD在后台调用一个block, 或者定义一个block来描述一个需要多次重复执行的任务, 比如遍历集合的时候. 并发和遍历在文章后面也会简单提及.

Block也经常被用于回调, 在一个任务结束的时候执行一段特定的代码. 举个例子, 我们的APP需要进行一项复杂的任务来响应用户的动作, 比如从网络请求数据. 因为这个任务可能花费很长的时间, 我们就需要在任务进行时给用户展示一个进度指示器, 而在任务结束时把它隐藏起来.

可以试着用代理来实现这个需求: 我们需要创建一个协议, 实现协议方法, 指定代理对象, 并且在任务完成时让代理对象调用协议方法. 这看起来相当繁琐, 而使用Block则会简单得多, 我们可以在创建任务的同时就创建任务完成时的回调:

- (IBAction)fetchRemoteInformation:(id)sender {
    [self showProgressIndicator];

    XYZWebTask *task = ...

    [task beginTaskWithCallbackBlock:^{
        [self hideProgressIndicator];
    }];
}

这个例子调用了一个方法来展示进度指示器, 然后创建了一个任务并且开始执行, 而block回调则包含了任务完成时需要执行的代码(比如隐藏进度指示器). 注意, 为了调用hideProgressIndicator方法, block捕获了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(1.5, 2.5);
}

Block永远应该作为方法的最后一个参数

最好的实践是, 在方法中最多只有一个block参数. 如果方法中还有其他非block的参数, 那么block参数应该写在最后面:

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

这可以使得方法调用更容易阅读:

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

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

如果你需要定义具有相同签名的多个block, 那么可以对这个签名进行类型定义:
举个例子, 可以定义一个简单的block类型, 没有参数也没有返回值:

typedef void(^SimpleBlock)(void);

此时就可以用自定义的类型来创建block变量或block参数:

SimpleBlock anotherBlock = ^{
    // ...
};
- (void)beginTaskWithCallbackBlock:(SimpleBlock)callbackBlock {
    // ...
    callbackBlock();
}

Block作为对象的属性

定义一个block属性的语法和定义block变量的语法类似:

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

可以像其他block变量一样来设置和调用一个block属性:

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

当然, 可以使用类型定义来简化代码:

typedef void (^XYZSimpleBlock)(void);

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

捕获self时避免强引用循环

如果你需要在block中捕获self, 比如在定义一个block回调时, 那么考虑内存管理问题是非常重要的.

block会对它捕获到的对象维持一个强引用, 包括self, 这就意味着很容易发生强引用循环, 比如, 一个对象持有一个block属性, 而在block内部又捕获了self:

@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
    self.block = ^{
        [self doSomething];    // 捕获self的强引用, 形成强引用循环
    };
}
...
@end

对于上面的简单情况, 编译器会给出警告, 但是更复杂的情况就会大大增加诊断的难度.

为了避免这个问题, 最好的方式是捕获self的弱引用:

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];   // 捕获弱引用来避免强引用循环
    }
}

此时, block不会持有对self的强引用. 如果对象在调用block之前被销毁了, 那么weakSelf也会被自动设置为nil, 不会发生危险.

Block能简化遍历

许多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本身有3个参数, 前两个代表了当前的对象以及它在数组中的索引, 第三个参数是个指向布尔值的指针, 你可以用它来结束遍历:

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

NSDictionary类也提供了基于block的方法, 比如:

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

比起传统的循环语句, 这将更容易地遍历每个键值对.

在操作队列中使用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来监测任务的状态变化, 这就方便我们去更新进度指示器, 比如在任务完成的时候把它隐藏起来.

在派发队列中使用block

派发队列是由GCD控制的, 使用它可以让同步或者异步执行任务变得容易. 你可以创建自己的派发队列, 也可以使用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");
});

而dispatch_sync()函数则会等到block执行完毕才返回:

dispatch_sync(queue, ^{
    NSLog(@"Block for synchronous execution");
});

如果一个block需要等待另一个任务完成之后才能继续执行, 就可以使用dispatch_sync()函数.

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值