为什么ARC有时只保留__block指针?

时间:2016-04-01 07:52:41

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

1)为什么这会保留__block var:

{
    void (^blockWithOutPointer)(NSObject * __autoreleasing *) = ^(NSObject * __autoreleasing * outPointer) {
        *outPointer = [NSObject new];
    };

    NSObject * __block blockVar1;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 (int64_t)(1 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(),
                   ^{
                       NSLog(@"blockVar1: %@",
                             blockVar1);
                       // prints non-nil. WHY????
                   });
    blockWithOutPointer(&blockVar1);
}

2)但这不是吗?

void (^blockWithOutPointerThatDispatchesLater)(NSObject * __autoreleasing *,
                                               dispatch_block_t) = ^(NSObject * __autoreleasing * outPointer,
                                                                     dispatch_block_t block) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 (int64_t)(1 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(),
                   block);
    *outPointer = [NSObject new];
};

{
    NSObject * __block blockVar2;
    blockWithOutPointerThatDispatchesLater(&blockVar2,
                                           ^{
                                               NSLog(@"blockVar2: %@",
                                                     blockVar2);
                                           });
    // prints nil, which is expected.
}

3)如果我改为使用__autoreleasing变量作为我的指针目的地,然后将该变量分配给我的__block指针,一切正常。

{
    NSObject * __autoreleasing autoreleasingVar;
    NSObject * __block blockVar3;
    blockWithOutPointerThatDispatchesLater(&autoreleasingVar,
                                           ^{
                                               NSLog(@"blockVar3: %@",
                                                     blockVar3);
                                           });
    blockVar3 = autoreleasingVar;
    // prints non-nil, which is expected.
}

我读过CRD's answer about ARC pointer-to-pointer issues,因为ARC认为blockVar2__autoreleasing并且不保留其值,所以#2会打印为nil是有意义的。因此,在#3中,当我们将autoreleasingVar分配给blockVar3时,ARC会正确保留该值。但是,#1没有这样的任务。为什么#1会保留其价值?

更令人惊讶的是,如果我在@autoreleasepool中包装out-pointer赋值,#1不受影响:

{
    void (^blockWithOutPointer)(NSObject * __autoreleasing *) = ^(NSObject * __autoreleasing * outPointer) {
        @autoreleasepool {
            *outPointer = [NSObject new];
        }
    };

    NSObject * __block blockVar1;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 (int64_t)(1 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(),
                   ^{
                       NSLog(@"blockVar1: %@",
                             blockVar1);
                       // still prints non-nil. WHY???
                   });
    blockWithOutPointer(&blockVar1);
}

而#3崩溃,正如预期@autoreleasepool释放出指针的对象一样,我猜ARC并没有将__autoreleasing变量设置为nil

void (^blockWithOutPointerThatDispatchesLater)(NSObject * __autoreleasing *,
                                               dispatch_block_t) = ^(NSObject * __autoreleasing * outPointer,
                                                                     dispatch_block_t block) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 (int64_t)(1 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(),
                   block);
    @autoreleasepool {
        *outPointer = [NSObject new];
    }
};

{
    NSObject * __autoreleasing autoreleasingVar;
    NSObject * __block blockVar3;
    blockWithOutPointerThatDispatchesLater(&autoreleasingVar,
                                           ^{
                                               NSLog(@"blockVar3: %@",
                                                     blockVar3);
                                               // crashes on the NSLog!
                                           });
    blockVar3 = autoreleasingVar;
}

我就此提出了radar

2 个答案:

答案 0 :(得分:4)

你发现了一个"功能"块实现和(至少在1和2的情况下,我还没有进一步检查)它与__autoreleasing 本身无关

首先让我们来看看你的案例1.你似乎很惊讶这打印出一个非零值。它完全按预期工作:

    执行
  1. blockWithOutPointer(&blockVar1);,为blockVar1分配值;然后
  2. ^{ NSLog(@"blockVar1: %@", blockVar1); }由GCD执行,并且由(1)打印存储。
  3. (注意:您可以删除__autoreleasing限定符,它的工作方式相同,因为这是pass-by-writeback参数的推断模式。)

    现在你的案例2.这是你点击"功能":

    的地方
    1. 在Objective-C对象中堆分配;和
    2. Objective-C块是对象;所以
    3. Ergo块是堆分配的......
    4. 除非他们不是......
    5. 作为优化,块规范允许块及其捕获的__block变量进行堆栈分配,并且只有当它们的生命周期需要比堆栈帧的生命周期长时才移动到堆上

      作为优化它应该(a)对程序员来说基本上是不可见的 - 除了任何性能优势和(b)无论如何都不会改变语义。然而,Apple决定最初将其作为程序员辅助优化引入它。然后慢慢改善了事情。

      您的案例2的行为全部归结为块和__block变量被复制到堆上。我们来看看代码:

      NSObject * __block blockVar2;
      

      这声明blockVar2具有__block存储持续时间,允许块更改此本地声明的变量的值。此时,编译器不知道块是否可以访问它,在这种情况下blockVar2将需要在堆上,或者它是否胜过,在这种情况下它可能在堆栈上。

      编译器决定它更喜欢堆栈并在那里分配blockVar2

      blockWithOutPointerThatDispatchesLater(&blockVar2,
      

      现在编译器需要传递blockVar2的地址作为第一个参数,变量当前在堆栈上,编译器发出代码来计算其地址。该地址是在堆栈上

                                             ^{
                                                 NSLog(@"blockVar2: %@",
                                                       blockVar2);
                                             });
      

      现在编译器到达第二个参数。它看到块,块访问blockVar2blockVar2__block限定,因此它必须捕获变量本身和不< / strong>变量值。

      编译器决定该块应该在堆上。为此,需要将blockVar2迁移到堆上,因此它与当前值nil一起执行...

        

      <强>糟糕!

           

      第一个参数是堆栈上原始blockVar2的地址,而第二个参数是一个块,它反过来引用堆上的克隆blockVar2

      执行代码时blockWithOutPointerThatDispatchesLater()分配一个对象并将其地址存储在堆栈 blockVar2中;然后GCD执行延迟块,该块打印 blockVar2的值,即nil

      “修复”

      只需将您的代码更改为:

      NSObject * __block blockVar2;
      dispatch_block_t afterBlock = ^{
           NSLog(@"blockVar2: %@", blockVar2);
      };
      blockWithOutPointerThatDispatchesLater(&blockVar2, afterBlock);
      

      即。预先计算第二个参数表达式。现在编译器在看到&blockVar2之前看到该块,将块和blockVar2移动到堆,为&blockVar2生成的值是堆的地址 blockVar2的版本。

        

      扩展结论:错误或功能?

      原始答案只是说这显然是一个错误,而不是一个功能,并建议你提交一份错误报告,注意它之前已被报告过,但另一份报告不会受到伤害。

      然而,这可能有点不公平,是的,它是 bug ,问题是这个bug究竟是什么。考虑:

      • 在(目标)C变量中,在堆栈上或静态分配,并且分配的动态内存块在其生命周期内不会移动。

      • 编译器优化通常不应改变程序的含义或正确性。

      Apple已经实现了编译器优化 - 在堆栈中存储块和捕获的__block变量 - 以可以在其生命周期内移动__block属性变量的方式所以程序的意义和正确性可以改变。

      结果是程序语句的简单重新排序,其方式不应改变程序的含义或正确性。 这很糟糕!

      鉴于优化的历史,由Apple实施,其设计依赖于程序员的帮助(虽然它已经变得更加自动化),因为它的正确性可以简单地看作是所选实现的另一个“特征”

        

      <强>建议

      永远不要将(&)运算符的地址应用于__block存储持续时间的变量。如果你这样做 可以工作,但同样容易。

      如果您需要使用__block变量作为pass-by-writeback参数,请先将其复制到本地临时,然后再进行调用,最后再将其复制回来。

      HTH

答案 1 :(得分:1)

1)可能存在误解:

如果将变量导入到标有__block的块中,则编译器会生成包含各种字段的辅助结构以及与给定源对应的变量。也就是说,没有指针blockVar1,而是有一个完整的结构。

如果一个块导入了这个变量并且需要被复制(例如,当异步提交时)它也会将这个帮助器结构“移动”到堆上,然后将块本身“移动”到堆上。

本声明

NSObject * __block blockVar1;

将初始化辅助结构,该结构嵌入实际变量并将其初始化为nil。变量的相应地址指向堆栈。

当编译器解析此语句时:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                             (int64_t)(1 * NSEC_PER_SEC)),
               dispatch_get_main_queue(),
               ^{
                   NSLog(@"blockVar1: %@",
                         blockVar1);
                   // prints non-nil. WHY????
               });

编译器为复合语句生成代码,特别是它创建了一个结构,表示最初存在于堆栈中的块。由于此块需要在执行dispatch_async之前移动到堆,因此它还会在随后生成的代码中将帮助器结构移动到堆上。

此外,由于导入的变量是NSObject指针,它还会分配函数指针(位于辅助结构中),它们“保持”和“处置”对象,当辅助结构是当它被销毁时,分别“移动”到堆中。

当你最终执行这个陈述时

blockWithOutPointer(&blockVar1);

变量的地址已经被更改:变量现在位于堆上(因为帮助器结构已“移动”到堆“)并且只要存在块就存在。

现在,您要声明(并定义)以下块:

void (^blockWithOutPointer)(NSObject * __autoreleasing *)

这里导致抱怨的原因是__autoreleasing修饰符,特别是Storage duration of __autoreleasing objects

也就是说,__autoreleasing只能应用于自动存储持续时间的变量。

修改

这将假设给定变量在堆栈上。但是,它位于堆上。因此,您的程序格式错误,行为未定义。

关于__autoreleasing存储限定符,代码示例1)是正确的(_autoreleasing存储限定符是指将变量作为参数传递时创建的临时值,请参阅下面的注释。

我之前所说的应该是正确的,因此预期用户会遇到什么。

有关详细信息,请参阅:http://clang.llvm.org/docs/Block-ABI-Apple.html

然而

“数据竞赛”还存在一个微妙的潜在问题:声明

blockWithOutPointer(&blockVar1);

将在当前线程上执行时修改位于堆上的指针变量。稍后,在主线程上将读取相同的变量。除非当前线程等于主线程,否则这将展示经典的数据竞争。

虽然这不是问题,也不是一个完整的答案 - 它表明,这些代码变得比预期更复杂,我建议努力寻找更简单,易于理解的代码。

结束修改

我还没有分析其他代码示例

- 但乍看之下,使用__autoreleasing导致一个不正确的程序。