递归块保留环问题

20

这会导致任何保留环路吗?使用它是否安全?

__block void (^myBlock)(int) = [^void (int i)
{
    if (i == 0)
        return;

    NSLog(@"%d", i);
    myBlock(i - 1);
} copy];
myBlock(10);

myBlock = nil;

1
我已经发布了一种简单的方法来解决递归块中的保留循环。这也适用于ARC:https://dev59.com/questions/0m035IYBdhLWcg3wef9y#14730061 - Berik
6个回答

34

您的代码确实包含保留循环,但您可以在递归结束时通过在递归基本情况(i == 0)中将myBlock设置为nil来打破保留循环。

证明这一点的最佳方法是尝试在“分配”工具下运行它,关闭“停止时丢弃未记录的数据”,打开“记录引用计数”,并关闭“仅跟踪活动分配”。

我使用OS X命令行工具模板创建了一个新的Xcode项目。以下是整个程序内容:

#import <Foundation/Foundation.h>

void test() {
    __block void (^myBlock)(int) = [^void (int i){
        if (i == 0) {
//            myBlock = nil;
            return;
        }
        NSLog(@"myBlock=%p %d", myBlock, i);
        myBlock(i - 1);
    } copy];
    myBlock(10);
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        test();
    }
    sleep(1);
    return 0;
}

然后我在分配仪器下运行它,并使用我上面描述的设置。然后我在Instruments中将“Statistics”更改为“Console”,以查看程序输出:

2012-10-26 12:04:31.391 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 10
2012-10-26 12:04:31.395 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 9
2012-10-26 12:04:31.396 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 8
2012-10-26 12:04:31.397 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 7
2012-10-26 12:04:31.397 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 6
2012-10-26 12:04:31.398 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 5
2012-10-26 12:04:31.398 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 4
2012-10-26 12:04:31.399 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 3
2012-10-26 12:04:31.400 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 2
2012-10-26 12:04:31.401 recursiveBlockTest[71789:303] myBlock=0x7ff142c24700 1
<End of Run>

我复制了块地址 (0x7ff142c24700),将“Console”更改为“Objects List”,然后将该地址粘贴到搜索框中。Instruments 仅显示分配给该块的部分:

block leaked

Live 列下的点表示程序退出时该块仍处于分配状态。它被泄漏了。我点击地址旁边的箭头以查看块分配的完整历史记录:

block leaked detail

这个分配只发生过一件事:它被分配了。

接下来,我取消了 if (i == 0) 语句中的 myBlock = nil 行的注释。然后我再次在性能分析器下运行了它。系统会出于安全考虑随机化内存地址,因此我清空了搜索栏,然后再次检查 Console 以获取此次运行中块的地址。这一次它是 0x7fc7a1424700。我再次切换到“Objects List”视图,并粘贴新地址 0x7fc7a1424700。这是我看到的:

block freed

这一次 Live 列下没有点,这意味着该块在程序退出时已经被释放。然后我点击地址旁边的箭头以查看完整的历史记录:

block freed detail

这一次,该块被分配、释放和释放。


1
在这种情况下,即使块具有 __block 存储说明符,为什么它仍然被强制捕获? - Ramy Al Zuhouri
请阅读clang ARC文档第7.5节。其中提到:“推断规则同样适用于__block变量,这是从非ARC转移过来的语义,其中__block变量在捕获期间不会隐式保留。” “Inference”是指向§4.4的链接,该链接说明:“如果一个对象声明为可保留对象所有者类型,但没有显式的所有权限定符,则其类型会被隐式调整为具有__strong资格认证。” - rob mayoff
因此,在ARC下,没有显式所有权限定符的__block变量被限定为__strong,并保留其所引用的对象。 - rob mayoff
4
问题在于,尽管您的程序块实际上已被释放,但 ARC 仍然会发出“强引用' myBlock '在此块中的捕获可能导致保留周期”的警告。 - SG1
是的,我在尝试这个时也看到了相同的警告。最终我使用了下面tc的解决方案。效果不错。 - stuckj

14

有一个简单的解决方案可以避免循环和可能需要过早复制的情况:

void (^myBlock)(id,int) = ^(id thisblock, int i) {
    if (i == 0)
      return;

    NSLog(@"%d", i);
    void(^block)(id,int) = thisblock;
    block(thisblock, i - 1);
  };

myBlock(myBlock, 10);

你可以添加一个包装器来恢复原始的类型签名:

void (^myBlockWrapper)(int) = ^(int i){ return myBlock(myBlock,i); }

myBlockWrapper(10);

如果你想将它扩展为相互递归,那么这将变得繁琐,但我想不到一开始就要这样做的好理由(用类可能更清晰明了)。


非常棒的解决方案,非常适合重复动画多次。 - Lucien

2
如果你正在使用ARC,那么你有一个保留循环(retain cycle),因为__block对象变量被块(block)所保留。这样,该块将保留自身。你可以通过将myBlock声明为__block__weak来避免这种情况。
如果你正在使用MRC,__block对象变量不会被保留,因此你不应该有问题。只需记得在最后释放myBlock即可。

1

我希望有一个不会出现警告的解决方案,在这个帖子https://dev59.com/Y2445IYBdhLWcg3wgqnT#17235341中Tammo Freese提供了最好的解决方案:

__block void (__weak ^blockSelf)(void);
void (^block)(void) = [^{
        // Use blockSelf here
} copy];
blockSelf = block;
    // Use block here

他的解释非常有道理。

1
不会导致保留循环。__block关键字告诉块不要复制myBlock,在赋值之前会发生这种情况,导致应用程序崩溃。如果这不是ARC,您需要做的唯一事情就是在调用myBlock(10)后释放myBlock。

完成后不要在块内释放它,而是在调用 myBlock(10) 后释放。我已经将这个添加到答案中了。 - Joe
如果这是ARC,那么就不需要删除myBlock,并且尝试使用Block_release删除它将导致EXEC_BAD_ACCESS错误,当ARC尝试释放它时,或者更糟糕的是,释放错误的对象。 - Richard J. Ross III
2
@pcperini,这一点完全不必要。ARC会自动处理这个问题。 - Richard J. Ross III
1
Joe,这个回答对于 ARC 代码来说完全是错误的,而 OP 的目标似乎就是 ARC。有关更多信息,请阅读此处的答案:https://dev59.com/QGPVa4cB1Zd3GeqP1gGK?rq=1 。 __block 在 ARC 下确实会复制其内容,不像你所说的,而且在 ARC 下不需要释放块。 - Richard J. Ross III
1
在这个意义上,“copy”表示通过值来捕获变量(这是块默认情况下所做的)。__block使其通过引用捕获变量。 - Richard J. Ross III
显示剩余8条评论

0

这里是一个现代化的解决方案:

void (^myBlock)();
__block __weak typeof(myBlock) weakMyBlock;
weakMyBlock = myBlock = ^void(int i) {
    void (^strongMyBlock)() = weakMyBlock; // prevents the block being delloced after this line. If we were only using it on the first line then we could just use the weakMyBlock.
    if (i == 0)
        return;

    NSLog(@"%d", i);
    strongMyBlock(i - 1);
};
myBlock(10);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接