为什么ARC有时仅保留__block out指针? [英] Why does ARC only sometimes retain a __block out pointer?

查看:77
本文介绍了为什么ARC有时仅保留__block out指针?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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关于ARC指针对指针问题的答案,可以理解#2可以打印零,因为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释放出指针对象以来的预期,并且我猜想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;
}

我对此提交了雷达.

解决方案

您已经发现了块实现的功能",并且(至少对于情况1和2,我没有进一步检查)使用__autoreleasing 本身.

首先让我们看一下您的情况1.您似乎很惊讶,它打印出一个非nil值.它完全按预期工作:

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

(注意:您可以删除__autoreleasing限定词,其工作原理与推断传递参数的推断模式相同.)

现在,您的案例2.这是您点击功能"的地方:

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

作为优化 ,块规范允许对块及其捕获的__block变量进行堆栈分配,并且仅在其生存期需要长于堆栈帧的时间时才移动到堆上. /p>

作为一种优化,它应该(a)对于程序员来说基本上是不可见的-除了任何性能方面的好处外,并且(b)无论如何都不能改变语义.但是,Apple决定最初将其作为程序员协助的优化"进行介绍,然后逐步改善问题.

情况2的行为完全取决于将块和__block变量复制到堆上时的情况.让我们看一下代码:

NSObject * __block blockVar2;

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

编译器决定优先使用堆栈,并在其中分配blockVar2.

blockWithOutPointerThatDispatchesLater(&blockVar2,

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

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

现在,编译器进入第二个参数.它看到该块,该块访问blockVar2,并且blockVar2__block限定,因此它必须捕获变量本身而变量值

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

一起进行迁移.

糟糕!

第一个参数是堆栈上原始blockVar2的地址,第二个参数是一个块,该块依次引用堆上克隆的blockVar2.

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

修复"

只需将代码更改为:

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

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

扩展结论:是错误还是功能?

原始答案只是简单地指出这是一个错误,而不是功能,建议您提交一个错误报告,并指出该错误已被报告过,但另一报告不会造成损害.

但是这可能有点不公平,是的,这是一个 bug ,问题是该错误实际上是什么.考虑:

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

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

Apple已实现了编译器优化-将块和捕获的__block变量存储在堆栈上-以一种方式 可以在其生存期内移动__block属性变量,这样做的含义和程序的正确性可以更改.

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

考虑到Apple实施的优化的历史,Apple出于设计的正确性而依赖于程序员的帮助(尽管此后它已变得更加自动化),这可以简单地视为所选实现的另一个功能"

推荐

永远不要将(&)的地址运算符应用于具有__block存储持续时间的变量.如果可以可行,那么也可以.

如果您需要使用__block变量作为传递回写参数,则先将其复制到本地临时目录,进行调用,最后再将其复制回.

HTH

1) Why does this retain its __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) But this doesn't?

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) If I instead use an __autoreleasing variable as my out pointer destination, and then assign that variable to my __block pointer, everything works fine.

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

I've read CRD's answer about ARC pointer-to-pointer issues, it makes sense that #2 would print nil because ARC assumes that blockVar2 is __autoreleasing, and doesn't retain its value. Thus, in #3, when we assign autoreleasingVar to blockVar3, ARC properly retains the value. However, there is no such assignment for #1. Why does #1 retain its value?

Even more amazingly, #1 is unaffected if I wrap the out-pointer assignment in an @autoreleasepool:

{
    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);
}

Whereas #3 crashes, as expected since the @autoreleasepool released the out-pointer's object, and I guess ARC doesn't set __autoreleasing variables to 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;
}

I filed a radar about this.

解决方案

You have discovered a "feature" of the block implementation and (at least for cases 1 & 2, I haven't checked further) it is nothing to do with __autoreleasing per se.

First let's look at your case 1. You seem surprised this prints a non-nil value. It works exactly as expected:

  1. blockWithOutPointer(&blockVar1); executes, assigning a value to blockVar1; then
  2. the block ^{ NSLog(@"blockVar1: %@", blockVar1); } is executed by GCD and the stored by (1) is printed.

(Note: You can removed the __autoreleasing qualifiers and it will work the same as this is the inferred mode for pass-by-writeback parameters.)

Now your case 2. This is where you hit the "feature":

  1. In Objective-C objects are heap allocated; and
  2. Objective-C blocks are objects; so
  3. Ergo blocks are heap allocated...
  4. Except when they are not...

As an optimisation the block specification allows blocks and their captured __block variables to be stack allocated and only moved onto the heap when their lifetime needs to be longer than the stack frame are in.

As an optimisation it should (a) be essentially invisible to the programmer - apart from any perf benefit and (b) not change the semantics in anyway. However Apple decided to initially introduce it as a "programmer assisted optimisation" and have then slowly improved matters.

The behaviour of your case 2 is all down to when the block and __block variable get copied onto the heap. Let's look at the code:

NSObject * __block blockVar2;

This declares blockVar2 to have __block storage duration, which allows a block to alter the value of this locally declared variable. At this point the compiler does not know whether a block may access it, in which case blockVar2 will need to be on the heap, or whether it won't, in which case it may be on the stack.

The compiler decides it prefers the stack and allocates blockVar2 there.

blockWithOutPointerThatDispatchesLater(&blockVar2,

Now the compiler needs to pass the address of blockVar2 as the first argument, the variable is currently on the stack, and the compiler emits code to calculate its address. That address is on the stack.

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

Now the compiler gets to the second argument. It sees the block, that the block accesses blockVar2, and that blockVar2 is qualified by __block so it must capture the variable itself and not the variables value.

The compiler decides the block should go on the heap. For this to work it needs to migrate blockVar2 onto the heap, so it does that along with its current value 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.

When the code is executed blockWithOutPointerThatDispatchesLater() allocates an object and stores its address in the stack blockVar2; then GCD executes the delayed block which prints of the value of the heap blockVar2, which is nil.

A "Fix"

Just change your code to:

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

i.e. pre-calculate the second argument expression. Now the compiler sees the block before it sees &blockVar2, moves the block and blockVar2 to the heap, and the value generated for &blockVar2 is the address of the heap version of blockVar2.

Expanded Conclusion: Bug or Feature?

The original answer simply stated this is clearly a bug, not a feature, and suggest you file a bug report, noting it's been reported before but another report won't harm.

However that might be a little unfair, yes it is a bug, the question is what the bug actually is. Consider:

  • In (Objective-)C variables, allocated on the stack or statically, and allocated dynamic memory blocks do not move during their lifetime.

  • Compiler optimisations in general should not alter the meaning or correctness of a program.

Apple has implemented a compiler optimisation – storing blocks and captured __block variables on the stack – in a way which can move __block attributed variables during their lifetime and in doing so the meaning and correctness of a program can be altered.

The result is a simple re-ordering of program statements in a way which should not alter program meaning or correctness in fact does. This is bad!

Given the history of the optimisation, as implemented by Apple, which by-design relied on programmer assistance (though it has since been made more automatic) for its correctness this could simply be seen as another "feature" of the chosen implementation.

Recommendation

Never, ever, apply the address-of (&) operator to a variable with __block storage duration. If may work if you do, but it just as easily may not.

If you need to use a __block variable as a pass-by-writeback argument then copy it to a local temporary first, make the call, and finally copy it back again.

HTH

这篇关于为什么ARC有时仅保留__block out指针?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆