NSBlockOperation面试与正确用法

3,624 阅读4分钟

0. 引言

在日常开发中,我们可能会用到 NSBlockOperation 来做一些多线程的操作。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [MyBlockOperation blockOperationWithBlock:^{
    // do something
}];
[queue addOperation:operation];

因为其使用简单,所以也被广泛使用。

但这个方法也有一个缺点,当 operation 开始执行后,[operation cancel]; 并不能及时取消。

同时,blockOperationWithBlock 没有 operation 的回调参数。有些同学想要通过 operation.isCancelled 进行任务的及时取消,不太清楚应该怎么做,要是使用不当甚至会引入 循环引用 问题。

下面,我们通过几个面试题来一步步了解清楚这个问题。

1. 面试题

1.1 面试题1

下面的代码输出什么?

  • cancelled
  • finish
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    sleep(2);
    if (operation.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});

答案

  • cancelled
  • finish

分析

初始化时传入了一个 block,block 中含有 operation 对象的访问,即发生了对象的捕获。但是现在是对象的创建过程中,operation 还没有被初始化出来,block这时捕获的是 operation 当前的值,即 operation = nil

1.2 面试题2

下面的代码输出什么?

  • cancelled
  • finish
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    sleep(2);
    if (operation.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});

答案

  • cancelled
  • finish

分析

这次加入了 __block 修饰符,捕获变为了指针引用,所以这次 [operation cancel]; 生效了。

简单来说就是:

  • 未使用 __block 修饰符,捕获的是当前值。

    int i = 0;
    void(^block)(void) = ^{
        i = 1;
        printf("%d", i); // 1
    };
    i = 2;
    block();
    

    上面这个问题已经有很多文章了,这里不深入说明。

    NSString *str = @"0";
    void(^block)(void) = ^{
        NSLog(@"%@", str); // 0
    };
    str = @"1";
    block();
    

    部分同学可能对这个有疑问。其实,两种捕获的原理都是对变量的 “值引用” 进行捕获,捕获的是当前对象的值。之后的修改,不会影响到 block 内部的值。

详细的原理可以参看我以前的文章《一道Block面试题的深入挖掘》

1.3 面试题3

下面的代码输出什么?

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled
@interface MyBlockOperation : NSBlockOperation
@end

@implementation MyBlockOperation
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}
@end

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block MyBlockOperation *operation = [MyBlockOperation blockOperationWithBlock:^{
    sleep(2);
    if (operation.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});

答案

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled

分析

刚刚分析了 __block 修饰符,捕获变为了指针引用。现在 operation 对象持有 block ,同时 block 通过指针引用捕获了 operation 对象,所以发生了循环引用。

那可能有些同学就要说了,加上 __weak 不就好了?

1.4 面试题4

下面的代码输出什么?

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled
  • 只有 -[MyBlockOperation dealloc]
@interface MyBlockOperation : NSBlockOperation
@end

@implementation MyBlockOperation
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}
@end

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block __weak MyBlockOperation *operation = [MyBlockOperation blockOperationWithBlock:^{
    sleep(2);
    if (operation.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});

答案

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled
  • 只有 -[MyBlockOperation dealloc]

这里加入了 __weak 修饰符,对象创建后,发现引用计数为 0 就被释放了,所以 block 不执行。这部分属于内存管理的知识点。

那么我们要怎么做,才能既捕获到 operation 又不会导致循环引用呢?

1.5 面试题5

下面的代码输出什么?

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled
  • 只有 -[MyBlockOperation dealloc]
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block __weak MyBlockOperation *wkOp;
MyBlockOperation *operation = [MyBlockOperation blockOperationWithBlock:^{
    sleep(2);
    if (wkOp.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
wkOp = operation;
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});

答案

  • cancelled, -[MyBlockOperation dealloc]
  • 只有 cancelled
  • 只有 -[MyBlockOperation dealloc]

分析

为了让 block 访问到初始化之后的值,wkOp 用了 __block 修饰符;同时,为了避免循环引用问题,wkOp 还用了 __weak 修饰符。

operation 初始化后被赋值,这里会有一个隐式的 __strong 修饰,即这里会对 operation 进行强引用,引用计数 +1 。wkOp 为弱引用对象,wkOp = operation; 只是用一个弱引用指针指向 operation ,引用计数不变。与之前不同的是,此时的 operation 的引用计数大于 0,所以不会被立刻释放。

2. 封装

为了日常使用方便封装一下就更香了~

@interface MyBlockOperation : NSBlockOperation

+ (instancetype)blockOperationWithBlock:(void (^)(void))block NS_UNAVAILABLE;
+ (instancetype)blockOperationWithOperationBlock:(void (^)(MyBlockOperation *))block;

@end

@implementation MyBlockOperation
- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}

+ (instancetype)blockOperationWithOperationBlock:(void (^)(MyBlockOperation *))block;
{
    __block __weak MyBlockOperation *wkOp;
    MyBlockOperation *op = [super blockOperationWithBlock:^{
        !block ?: block(wkOp);
    }];
    wkOp = op;
    return op;
}

@end

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
MyBlockOperation *operation = [MyBlockOperation blockOperationWithOperationBlock:^(MyBlockOperation *op){
    sleep(2);
    if (op.isCancelled) {
        NSLog(@"cancelled");
    } else {
        NSLog(@"finish");
    }
}];
[queue addOperation:operation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [operation cancel];
});


如果觉得本文对你有所帮助,给我点个赞吧~ 👍🏻