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

Why does ARC only sometimes retain a __block out pointer?

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,#2 打印 nil 是有道理的,因为 ARC 假设 blockVar2__autoreleasing,并且不保留它的值。因此,在#3 中,当我们将 autoreleasingVar 分配给 blockVar3 时,ARC 正确地保留了该值。但是,#1 没有这样的分配。为什么#1 保留其价值?

更令人惊讶的是,如果我将外指针赋值包装在 @autoreleasepool:

中,#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 释放了 out-pointer 的对象,我猜 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

1中可能存在误解):

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

如果块导入此变量并需要复制(例如,当异步提交时),它还会 "moves" 在将块本身 "moving" 放入堆之前将此帮助程序结构放入堆中。

此声明

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指针,它还分配了"keep"和"dispose"对象的函数指针(位于辅助结构中),这将是当 helper 结构 "moved" 到堆时调用,分别在它被销毁时调用。

当你最终执行这条语句时

blockWithOutPointer(&blockVar1);

变量的地址已经改变:变量现在位于堆上(因为 helper struct 已经 "moved" 到堆上)并且只要块存在就存在。


现在,到您声明(并定义)以下块的地步:

void (^blockWithOutPointer)(NSObject * __autoreleasing *)

引起抱怨的是 __autoreleasing 修饰符,特别是 Storage duration of __autoreleasing objects:

__autoreleasing只能应用于自动存储时长的变量。

编辑:

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

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

我上面说的应该是正确的,所以用户的体验是意料之中的。

如需进一步参考,请参阅:http://clang.llvm.org/docs/Block-ABI-Apple.html

然而:

"data race" 仍然存在一个微妙的潜在 问题:语句

blockWithOutPointer(&blockVar1);

将修改位于堆上的指针变量,同时在当前线程上执行。稍后,在 主线程 上将读取相同的变量。除非 current thread 等于 main thread,这表现出经典的数据竞争。

虽然这不是问题,也没有完整的答案 - 它表明,这样的代码变得比预期的复杂得多,我建议争取更简单、易于理解的代码。

结束编辑


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

- 但乍一看,使用 __autoreleasing 似乎也会导致程序格式错误。

您发现了块实现的一个“特征”(至少对于情况 1 和 2,我没有进一步检查)它与 __autoreleasing per 无关se.

首先让我们看看您的案例 1。您似乎对打印非零值感到惊讶。它完全符合预期:

  1. blockWithOutPointer(&blockVar1);执行,给blockVar1赋值;然后
  2. ^{ NSLog(@"blockVar1: %@", blockVar1); }由GCD执行并打印(1)存储的内容。

(注意:您可以删除 __autoreleasing 限定符,它的工作方式与此相同,这是 pass-by-writeback 参数的推断模式。)

现在你的情况 2。这是你点击“功能”的地方:

  1. 在Objective-C中对象是堆分配的;和
  2. Objective-C 块是对象;所以
  3. Ergo 块是堆分配的...
  4. 除非他们不是...

作为优化,块规范允许块及其捕获的__block变量进行堆栈分配,并且只有在它们的生命周期需要长于堆栈框架在。

作为一项优化,它应该 (a) 基本上对程序员不可见——除了任何性能优势之外,以及 (b) 无论如何都不改变语义。然而,Apple 决定最初将其作为“程序员辅助优化”引入,然后慢慢改进。

案例 2 的行为完全取决于块和 __block 变量何时被复制到堆上。我们看代码:

NSObject * __block blockVar2;

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

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

blockWithOutPointerThatDispatchesLater(&blockVar2,

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

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

现在编译器到达第二个参数。它看到块,块访问 blockVar2,并且 blockVar2__block 限定,因此它必须捕获 变量 本身和 不是变量值。

编译器决定块应该放在堆上。为此,它需要将 blockVar2 迁移到堆上,因此它会连同其当前值 nil...

Oops!

The first argument is the address of the original blockVar2 on the stack, while the second argument is a block which in turn references the cloned blockVar2 on the heap.

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

“修复”

只需将您的代码更改为:

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

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

Expanded Conclusion: Bug or Feature?

原始答案只是简单地说明这显然是一个错误,而不是一个功能,建议您提交错误报告,注意之前已经报告过,但另一个报告不会造成伤害。

然而这可能有点不公平,是的,这是一个 bug,问题是这个 bug 到底是什么。考虑:

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

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

Apple 已经实现了编译器优化——在堆栈上存储块和捕获的 __block 变量——在某种程度上 可以 移动 __block 属性变量它们的生命周期以及这样做可以改变程序的意义和正确性。

结果是以一种不应改变程序含义或正确性的方式对程序语句进行简单的重新排序。 这很糟糕!

鉴于 Apple 实施的优化历史,其正确性在设计上依赖于程序员的协助(尽管此后变得更加自动化),这可以简单地视为另一个“功能”选择的实现。

Recommendation

永远永远不要将寻址 (&) 运算符应用于具有 __block 存储持续时间的变量。如果你这样做,可能 会起作用,但它可能不会起作用。

如果您需要使用 __block 变量作为传递回写参数,那么首先将其复制到本地临时文件,进行调用,最后再将其复制回来。

HTH