*精确*是否需要在ARC下复制Objective-C中的块?

时间:2013-10-16 00:37:50

标签: objective-c automatic-ref-counting objective-c-blocks

我在使用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:^(...){
        ...
    }];
}

(The actual code this snipper was adapted from is here.)

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

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

3 个答案:

答案 0 :(得分:3)

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

首次审核何时需要在MRC下复制块是有益的。基本上,捕获变量的块可以从堆栈开始。这意味着当您看到块文字时,编译器可以用包含对象结构本身的范围中的隐藏局部变量替换它。由于局部变量仅在声明它们的范围内有效,因此块文字中的块仅在文字所在的范围内有效,除非它被复制。

此外,还有一个附加规则,即如果函数采用块指针类型的参数,则不会假设它是否是堆栈块。只保证块在调用块时有效。但是,这几乎意味着该块在函数调用的整个持续时间内都是有效的,因为1)如果它是一个堆栈块,并且在调用该函数时它是有效的,这意味着该块在块的哪个位置。创建后,调用仍然在堆栈文字的范围内;因此,在函数调用结束时它仍然在范围内; 2)如果它是堆块或全局块,则它与其他对象具有相同的内存管理规则。

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

  • 如果来自块文字的块从函数返回:它需要被复制,因为块从文字的范围中逃脱
  • 如果来自块文字的块存储在实例变量中:需要复制它,因为块从文字范围中逃脱
  • 如果块被另一个块捕获:它不需要被复制,因为捕获块(如果被复制)将保留所有捕获的对象类型变量并复制块类型的所有捕获变量。因此,我们的块将逃避此范围的唯一情况是,如果捕获它的块逃脱了范围;但为了做到这一点,必须复制该块,然后复制我们的块。
  • 如果来自块文字的块被传递给另一个函数,并且该函数的参数是块指针类型:它不需要被复制,因为该函数不假定它被复制了。这意味着任何需要阻止并且需要将其存储以供以后使用的功能"必须负责复制块。事实确实如此(例如dispatch_async)。
  • 如果来自块文字的块传递给另一个函数,并且该函数的参数是的块指针类型(例如-addObject:):它需要是如果你知道这个函数存储它以供以后复制。它需要被复制的原因是该函数不能对复制块负责,因为它不知道它是在阻塞。

因此,如果问题中的代码位于MRC中,则-whatever不需要复制任何内容。 -whenSettledDo:需要复制该块,因为它被传递给addObject:,这是一种采用通用对象的方法,类型为id,并且不知道它采取阻止。


现在,让我们来看看ARC为您提供的这些副本中的哪一个。 Section 7.5

  

除了在初始化__strong时完成的保留   参数变量或读取__weak变量,只要这些   语义调用保留块指针类型的值,它具有   Block_copy的效果。优化器可以在删除时删除这些副本   看到结果仅用作呼叫的参数。

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

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

直接使用settledHandler时,由于settledHandler是参数,因此不会自动复制,因此当您将其传递给addObject:时,需要明确复制它,因为我们讨论了传递给addObject:的块需要复制。

总而言之,在ARC中,您需要在将块传递给不具体使用块参数的函数时显式复制(如addObject:),如果它&# 39; sa block literal,或者它是你传递的参数变量。

答案 1 :(得分: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结尾,而从数组中删除的闭包中的NSMallocBlock的地址以7ed0结尾。他们是独特的。删除一个并不算除去另一个。

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

答案 2 :(得分: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];

已经太晚了!块已经离开了它的范围({bracket}),事情可能会出错。这可以通过不使用{bracket}来解决,但这是您自己调用copy的罕见情况之一。

当您将某个街区存放在某个地方时,99.99%的时间您将离开宣布该街区的范围;通常通过制作块属性来解决这个问题&#34; copy&#34;属性。如果你调用dispatch_async等,则需要复制块,但被调用的函数会这样做。 NSArray和NSDictionary的基于块的迭代器通常不必复制块,因为您仍然在声明块的范围内运行。

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