【问题标题】:When *exactly* is it necessary to copy a block in objective-C under ARC?何时*确切*需要在 ARC 下复制 Objective-C 中的块?
【发布时间】:2013-10-16 00:37:50
【问题描述】:

关于在 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 为何或何时不能正确处理块?它保证什么,我的责任是什么?所有这些都以非模糊的方式记录在哪里?

【问题讨论】:

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


    【解决方案1】:

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

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

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

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

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

    【讨论】:

    • 谢谢,这是对所涉及内容的很好的报道。您能否评论一下是否保证分配给本地人意味着必要的副本,或者这是否是一个可能会改变的实现细节?关于优化器删除副本的语言让我觉得我需要在分配到本地之前显式复制。
    • @Strilanc:我认为分配给块指针类型的本地会复制它。我不确定有关“结果仅用作调用的参数”的部分是否仅适用于您直接传递块文字时,或者它是否还包括仅在一个地方使用的本地作为参数.
    【解决方案2】:

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

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

    -(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,我想我以后需要注意这一点。

    【讨论】:

      【解决方案3】:

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

      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] 当块已经被复制时不做任何事情,它只是返回块本身。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-07-24
        • 2012-02-10
        • 1970-01-01
        • 2019-07-24
        • 1970-01-01
        • 2012-05-22
        • 1970-01-01
        相关资源
        最近更新 更多