关于Objective-C的block和copy的一些问题

5

我写了一些使用Objective-C block的代码,但是结果让我感到困惑。

@interface MyTest : NSObject

@end

@implementation MyTest

- (void)test {
    NSArray *array = [self array1];  // ok
//    NSArray *array = [self array2];// crash
//    NSArray *array = [self array3];// ok again

    dispatch_block_t block0 = (dispatch_block_t)[array objectAtIndex:0];
    block0();

    dispatch_block_t block1 = (dispatch_block_t)[array objectAtIndex:1];
    block1();
}

- (NSArray *)array1 {
    int a = 10;
    NSMutableArray *array = [NSMutableArray array];
    [array addObject:^{
        NSLog(@"block0: a is %d", a);
    }];
    [array addObject:^{
        NSLog(@"block1: a is %d", a);
    }];
    return array;
}

- (NSArray *)array2 {
    int a = 10;

    return [NSArray arrayWithObjects:^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }, nil];
}

- (NSArray *)array3 {
    int a = 10;

    return [NSArray arrayWithObjects:^{
        NSLog(@"block0: a is %d", a);
    },[^{
        NSLog(@"block0: a is %d", a);
    } copy], nil];
}
@end

我感到困惑的是:

  1. 数组array2为什么会崩溃?array1和array2真正的区别是什么?
  2. 我读过一些文章,说块拷贝会将块从栈移到堆中,但在array1方法中,我没有进行拷贝,它仍然可以正常工作。在array3中,我只是拷贝第二个块,它就可以正常工作。为什么呢?
  3. 在使用块时,我必须在哪里使用拷贝?

顺便说一下,我在ARC下的Xcode 4.6中运行了代码。谢谢。

3个回答

4
你似乎发现了编译器无法处理的块与类型丢失相关的情况。但我们需要从头开始...

以下涉及使用ARC下的块。其他情况(MRC,GC)不予考虑。

一些块被创建在堆栈上而不是堆上,这是一种优化,理论上可以以程序员无需意识到的方式实现。然而,在引入块时,决定不将优化透明化给用户,因此引入了blockCopy()。自那时以来,规范和编译器都已经发展(编译器实际上超出了规范),并且blockCopy()在它曾经需要的地方不再是(根据规范),在其他地方可能不再是(因为编译器可能超出规范)。

如何透明地实现优化?

请考虑:

  1. 编译器在创建栈分配块时会知道,并且
  2. 编译器知道何时将这样的块分配给另一个变量
  3. 那么编译器能否为每个赋值找出是否需要将该块移动到堆中?

平凡的答案是“可以” - 在任何赋值操作中移动到堆上。但这将否定优化的整个目的 - 创建一个堆栈块,将其传递给另一个方法,其中涉及对参数的赋值...

简单的答案是“不要尝试” - 引入blockCopy()并让程序员自己解决。

更好的答案是“可以”,但要聪明地做。伪代码如下:

// stack allocated block in "a", consider assignment "b = a"
if ( b has a longer lifetime than a )
{
   // case 1: assigning "up" the stack, to a global, into the heap
   // a will die before b so we need to copy
   b = heap copy of a;
}
else
{
   if (b has a block type)
   {
      // case 2: assigning "down" the stack - the raison d'être for this optimisation
      // b has shorter life (nested) lifetime and is explicitly typed as a block so
      // can accept a stack allocated block (which will in turn be handled by this
      // algorithm when it is used)
      b = a;
   }
   else
   {
      // case 3: type loss - e.g. b has type id
      // as the fact that the value is a block is being lost (in a static sense)
      // the block must be moved to the heap
      b = heap copy of a;
   }
}

在引入块的介绍中,情况1和3需要手动插入blockCopy(),情况2则是优化发挥作用的地方。
然而,正如早期答案中所解释的那样,现在的规范涵盖了情况1,而编译器似乎涵盖了情况3,但没有已知的文档证实这一点。
(顺便说一下,如果你跟随那个链接,你会看到它包含一个关于这个主题的旧问题的链接。那里描述的情况现在已经自动处理了,它是上面情况1的一个例子。)
喘口气,我们回到问题中的例子:
  • array1array3array4 都是第三种情况的示例,即存在类型丢失。它们也是上一个问题中测试并发现当前编译器可以处理的场景。它们之所以有效并非偶然或幸运,而是编译器显式地插入了所需的块拷贝。但我不知道这是否在任何官方文档中有记录。
  • array2 也是第三种情况的示例,即存在类型丢失,但它是一种未在上一个问题中测试的变体 - 通过作为可变参数列表的一部分传递导致类型丢失。当前编译器似乎没有处理这种情况。因此,我们现在有了一个线索,说明为什么第三种情况的处理未被记录 - 处理不完整。

请注意,正如先前提到的,您可以测试编译器的行为 - 您甚至可以在代码中加入一些简单的测试,以便在测试失败时立即终止应用程序。因此,如果您愿意,可以根据您所知道的编译器当前自动处理的内容(迄今考虑的除可变参数函数外),编写代码,并在更新编译器且替代缺乏支持时中止代码。

希望这对您有所帮助并且讲得通!


1
所有这三个代码片段都会崩溃(尽管我怀疑在array3的第一个元素上缺少一个copy可能是一个疏忽)。如果你想让一个块超出创建它的范围而存在,就必须复制它。除非你确定一个方法会复制传递给它的对象,否则你需要自己复制它。

除非该方法或函数明确指出复制了该块,否则在传递给任何异步操作的方法或函数时,您应始终复制该块。 - bbum
你运行代码的条件是什么?当我将其作为简单的OSX程序的一部分在OSX 10.7.5下使用时,我得到了与OP相同的结果。 - rmaddy
无论它们在特定情况下是否起作用,它们仍然是错误的,因为arrayWithObjects:addObject:没有明确说明它们复制其参数。 - ipmcc
可能是真的,但我仍然很好奇你在什么条件下运行了代码。为了自己的学习,我想知道你和原帖作者以及我自己之间有什么不同的地方。 - rmaddy
我在ARC和非ARC下都尝试过。在我的回答中,我是指的非ARC情况。ARC的实现实际上是不透明的。当我想要确定性的答案时,我会关闭ARC。 - ipmcc
就我所知,我怀疑 ARC 正在复制第一个参数,因为在方法原型中它的类型被明确指定为 id。它无法知道接下来的可变参数的类型(例如 printf 中可变参数可以是任意类型),因此不会复制它们。 - ipmcc

0

我尝试了第四种情况,也完全正常:

- (NSArray *)array4 {
    int a = 10;

    return @[ ^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }
             ];
}

当然,这与以下代码相同:

- (NSArray *)array4 {
    int a = 10;

    id blocks[] = { ^{
        NSLog(@"block0: a is %d", a);
    }, ^{
        NSLog(@"block1: a is %d", a);
    }
    };
    NSUInteger count = sizeof(blocks) / sizeof(id);

    return [NSArray arrayWithObjects:blocks count:count];
}

所以唯一的问题就在于“array2”。该实现的关键点是您调用了arrayWithObject:方法,该方法接受可变数量的参数。

似乎只有第一个(命名)参数被正确复制。没有任何可变参数被复制。如果添加第三个块,则仍然会在第二个块上出现问题。只有第一个块被复制。

因此,使用具有可变参数构造函数的块时,实际上只复制了第一个命名参数。没有任何可变参数被复制。

在创建数组的所有其他方法中,每个块都被复制。

顺便说一下-我在Lion(10.7.5)下使用ARC在Xcode 4.6.2中运行了您的代码和我的添加,并在简单的OS X应用程序中使用iOS 6.1应用程序时获得了相同的结果。


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