1

在 Objective-C 中使用 ARC 时,我收到了关于何时需要复制块的相互矛盾的信息。建议从“总是”到“从不”不等,所以我真的不知道该怎么做。

碰巧遇到一个不知道怎么解释的案例:

-(RemoverBlock)whenSettledDo:(SettledHandlerBlock)settledHandler {
    // without this local assignment of the argument, one of the tests fails. Why?
    SettledHandler handlerFixed = settledHandler;

    [removableSettledHandlers addObject:handlerFixed];

    return ^{
        [removableSettledHandlers removeObject:handlerFixed];
    };
}

使用这样的块内联调用:

-(void) whatever {
    [self whenSettledDo:^(...){
        ...
    }];
}

(这个sniper改编的实际代码在这里。)

在这里将参数复制到局部变量会发生什么变化?没有本地的版本是否制作了两个不同的副本,一个用于 addObject,一个用于 removeObject,因此删除的副本与添加的副本不匹配?

ARC为什么或何时不能正确处理块?它保证什么,我的责任是什么?所有这些都以非模糊的方式记录在哪里?

4

3 回答 3

3

在 C 中,无法通过运行任意数量的测试来推断正确性,因为您可能会看到未定义的行为。要正确知道什么是正确的,您需要查阅语言规范。在这种情况下,ARC 规范.

当需要在 MRC 下复制一个块时,首先审查是有益的。基本上,捕获变量的块可以从堆栈开始。这意味着当您看到块文字时,编译器可以将其替换为该范围内包含对象结构本身的隐藏局部变量,按值。由于局部变量仅在它们声明的范围内有效,这就是为什么来自块文字的块仅在文字所在的范围内有效,除非它被复制。

此外,还有一个附加规则,如果函数接受块指针类型的参数,它不会假设它是否是堆栈块。仅保证该块在调用该块时有效。然而,这几乎意味着该块在函数调用的整个持续时间内都是有效的,因为 1) 如果它是一个堆栈块,并且在调用函数时它是有效的,这意味着该块所在的堆栈上的某个位置创建时,调用仍在堆栈字面量范围内;因此在函数调用结束时它仍然在作用域内;2)如果是堆块或全局块,则与其他对象一样受制于相同的内存管理规则。

由此,我们可以推断出哪里需要复制。让我们考虑一些情况:

  • 如果从函数返回块文字中的块: 它需要被复制,因为块从文字的范围逃逸
  • 如果块文字中的块存储在实例变量中:它需要被复制,因为块从文字的范围中逃脱
  • 如果块被另一个块捕获:它不需要被复制,因为如果被复制,捕获块将​​保留所有捕获的对象类型的变量并复制所有捕获的块类型的变量。因此,我们的块会逃离这个范围的唯一情况是,如果捕获它的块逃离了这个范围;但为了做到这一点,必须复制该块,这反过来又复制了我们的块。
  • 如果块文字中的块被传递给另一个函数,并且该函数的参数是块指针类型:它不需要被复制,因为该函数不假定它被复制。这意味着任何需要一个块并需要“存储它以备后用”的函数都必须负责复制该块。确实是这样(例如dispatch_async)。
  • 如果块文字中的块被传递给另一个函数,并且该函数的参数不是块指针类型(例如-addObject:):如果您知道该函数将其存储以供以后使用,则需要复制它。它需要被复制的原因是函数不能负责复制块,因为它不知道它正在获取一个块。

因此,如果您在问题中的代码在 MRC 中,则-whatever不需要复制任何内容。-whenSettledDo:将需要复制块,因为它被传递给addObject:一个方法,该方法采用通用对象 type id,并且不知道它正在占用一个块。


现在,让我们看看这些副本中的哪些 ARC 会照顾您。第 7.5 节

除了作为初始化 __strong 参数变量或读取 __weak 变量的一部分完成的保留之外,每当这些语义要求保留块指针类型的值时,它都具有 Block_copy 的效果。当优化器发现结果仅用作调用的参数时,它可能会删除此类副本。

第一部分的意思是,在您分配给块指针类型的强引用的大多数地方(这通常会导致对象指针类型的保留),它将被复制。但是,也有一些例外: 1)在第一句的开头,它说不保证块指针类型的参数被复制;2)在第二句中,它说如果一个块仅用作调用的参数,则不能保证被复制。

这对您问题中的代码意味着什么?handlerFixed是块指针类型的强引用,结果在两个地方使用,不仅仅是一个调用的参数,因此分配给它分配一个副本。但是,如果您已将块文字直接传递给addObject:,则不能保证是副本(因为它仅用作调用的参数),并且您需要显式复制它(正如我们讨论的块传递toaddObject:需要复制)。

直接使用时settledHandler,由于settledHandler是参数,不会自动复制,所以在传递给 时addObject:,需要显式复制,因为正如我们所讨论的,传递给的块addObject:需要被复制。

因此,总而言之,在 ARC 中,您需要在将块传递给不专门采用块参数(如addObject:)的函数时显式复制,如果它是块文字,或者它是您传递的参数变量。

于 2013-10-29T10:27:12.907 回答
0

我已经确认我的特定问题实际上是制作了该块的两个不同副本。棘手棘手。这意味着正确的建议是“永远不要复制,除非您希望能够将块与自身进行比较”。

这是我用来测试它的代码:

-(void) testMultipleCopyShenanigans {
    NSMutableArray* blocks = [NSMutableArray array];
    NSObject* v = nil;
    TOCCancelHandler remover = [self addAndReturnRemoverFor:^{ [v description]; } 
                                                         to:blocks];
    test(blocks.count == 1);
    remover();
    test(blocks.count == 0); // <--- this test fails
}
-(void(^)(void))addAndReturnRemoverFor:(void(^)(void))block to:(NSMutableArray*)array {
    NSLog(@"Argument: %@", block);
    [array addObject:block];
    NSLog(@"Added___: %@", array.lastObject);
    return ^{
        NSLog(@"Removing: %@", block);
        [array removeObject:block];
    };
}

运行此测试时的日志输出为:

Argument: <__NSStackBlock__: 0xbffff220>
Added___: <__NSMallocBlock__: 0x2e283d0>
Removing: <__NSMallocBlock__: 0x2e27ed0>

参数是一个 NSStackBlock,存储在堆栈上。为了放置在数组或闭包中,它必须被复制到堆中。但是这种情况发生一次,用于添加到数组中,一次用于关闭。

所以数组中的 NSMallocBlock 的地址以 83d0 结尾,而从数组中删除的闭包中的地址以 7ed0 结尾。它们是不同的。删除一个不算作删除另一个。

Bleh,我想我将来需要注意这一点。

于 2013-10-16T01:00:51.683 回答
0

当应用程序离开定义块的范围时,必须复制块。一个不好的例子:

BOOL yesno;
dispatch_block_t aBlock;
if (yesno)
{
    aBlock = ^(void) { printf ("yesno is true\n");
}
else
{
    aBlock = ^(void) { printf ("yesno is false\n");
}
aBlock = [aBlock copy];

已经太晚了!该块已离开其范围({括号})并且可能会出错。这可以通过不使用 { 方括号 } 来轻松解决,但这是您自己调用 copy 的罕见情况之一。

当您将一个块存储在某个地方时,99.99% 的时间您都离开了声明该块的范围;通常这是通过使块属性“复制”属性来解决的。如果您调用 dispatch_async 等,则需要复制块,但被调用的函数会这样做。NSArray 和 NSDictionary 的基于块的迭代器通常不必复制块,因为您仍在声明块的范围内运行。

[aBlock copy] 当块已经被复制时不做任何事情,它只是返回块本身。

于 2014-03-24T15:25:22.037 回答