Block 的递归调用 - How to implement recursive call block?

有的时候我们需要在 Objective-C 方法里定义一个 Block, 并且这个 Block 还会递归调用自己。那有人就会问了,为什么不开一个函数用函数的递归调用呢?因为这个 Block 会引用到许多个 local variable,如果用函数调用的话,所有的值都要传入这个函数,将会非常难看,比如

// 注意,这段代码是错的
- (void)outerFunc {
    __block NSInteger localVar1 = 0;
    __block NSInteger localVar2 = 1;
    __block NSInteger localVar3 = 2;

    void (^duck)(NSInteger) = ^(NSInteger index) {
        localVar1++;
        localVar2--;
        localVar3 = localVar1 + localVar2;
        NSLog(@"%@", @(index));
        index--;
        if (index == 0) {
            return;
        }
        duck(index);
    };
    duck(10);
}

上面这段代码试图做一个 block 的递归调用,打印出10,9,8...1,并且在每一个调用里,都访问了一些本地变量。对于这样的需求,如果用函数的递归调用我们怎么搞?你得把本地变量的值传进函数,如果还需要修改这个本地变量的话,传值不行你还得传它的引用,总之就是特别麻烦。其实在 Javascript 这些函数是一等公民(First-class function)的语言里,对于这样的需求都是直接在方法里定义一个闭包搞定,如果有递归的话也是一样。 但是对于 Objective-C 来说,有递归就不一样了,比如上面的那段代码就是不能运行的。

运行上面的代码,会在递归调用 duck 的地方得到一个 BAD_EXCESS 的错误,这是很常见的非法访问内存的错误,原因是当执行 duck(10) 之后,代码进入到了块内部,这时 outerFunc 的作用域已结束,duck 被释放了,而在 块内部再去访问 duck 就崩溃了。

我们可以给块加上 __block 修饰符以防止块被释放

    void (^__block duck)(NSInteger) = ^(NSInteger index) {
    ...

但是这样又带来了引用循环(Retain Cycle)的问题,duck引用自己以至于递归结束后 duck 也不被释放。

我们可以在递归结束时手动把 duck 置为 nil,

    void (^__block duck)(NSInteger) = ^(NSInteger index) {
        localVar1++;
        localVar2--;
        localVar3 = localVar1 + localVar2;
        NSLog(@"%@", @(index));
        if (index == 0) {
            duck = nil;
            return;
        }
        duck(index);
    };
    duck(10);

这样看起来似乎是可以的,但很遗憾,如果在 outerFunc 里面只调用一次 duck 这样写是可以的,调用了 2 次或以上就不行了。比如

    for (int i = 10; i > 0; i--) {
        duck(i);
    }

当第一次递归调用结束的时候,duck 被置为 nil,但这时还有其他的递归调用没有执行完,访问到 duck 的时候还是崩. 于是乎我们得这么写

    __block __weak void(^weak_duck)(NSInteger);
    void (^duck)(NSInteger);
    weak_duck = duck = ^(NSInteger index) {
        localVar1++;
        localVar2--;
        localVar3 = localVar1 + localVar2;
        NSLog(@"%@", @(index));
        if (index == 0) {
            weak_duck(index);
            return;
        }
        duck(index);
    };
    duck(10);
    for (int i = 10; i > 0; i--) {
        duck(i);
    }

在outerFunc里面定义一个 weak 并且用 _block 修饰的指针指向这个块,进入这个块后,weakduck的引用计数就会增加保证 weak_duck 在所有的块递归结束后才被释放。

但遗憾的是,上面的代码还是不能 handle 所有的 case,比如在递归调用块的时候我们把递归执行的代码 dispatch_async 了一下,很抱歉,这时候weak 指针也挂了,如下:

    __block __weak void(^weak_duck)(NSInteger);
    void (^duck)(NSInteger);
    weak_duck = duck = ^(NSInteger index) {
        localVar1++;
        localVar2--;
        localVar3 = localVar1 + localVar2;
        NSLog(@"%@", @(index));
        if (index == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                weak_duck(index);
            });
            return;
        }
        duck(index);
    };
    for (int i = 10; i > 0; i--) {
        duck(i);
    }

所以最终的解法如下,在递归调用前再用一个 strong 指针指向 weak 指针所指的那个 block

- (void)outerFunc {
    __block NSInteger localVar1 = 0;
    __block NSInteger localVar2 = 1;
    __block NSInteger localVar3 = 2;

    __block __weak void(^weak_duck)(NSInteger);
    void (^duck)(NSInteger);
    weak_duck = duck = ^(NSInteger index) {
        void (^strong_duck)(NSInteger) = weak_duck;
        localVar1++;
        localVar2--;
        localVar3 = localVar1 + localVar2;
        NSLog(@"%@", @(index));
        if (index == 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                strong_duck(index);
            });
            return;
        }
        duck(index);
    };
    for (int i = 10; i > 0; i--) {
        duck(i);
    }
}

写了这么多,我最后只想说,fucking block syntax.

comments powered by Disqus