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指针对指针问题的答案,因为ARC假定blockVar2
为__autoreleasing
,并且不保留其值,所以#2将显示nil是有意义的。因此,在#3中,当我们分配autoreleasingVar
给时blockVar3
,ARC会正确保留该值。但是,#1没有这样的分配。为什么#1保留其价值?
更令人惊讶的是,如果将出站指针分配包装在#中,则#1不会受到影响@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);
}
而#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值。它完全按预期工作:
blockWithOutPointer(&blockVar1);
执行,为分配一个值blockVar1
;然后^{ NSLog(@"blockVar1: %@", blockVar1); }
由GCD执行,并打印由(1)存储的内容。(注意:您可以删除__autoreleasing
限定符,它的工作原理与推断传递回传参数的方式相同)。
现在您的案例2。这是您实现“功能”的地方:
作为一种优化,块规范允许对块及其捕获的__block
变量进行堆栈分配,并且仅在其生存期需要长于堆栈帧的寿命时才移动到堆上。
作为一种优化,它应该(a)对于程序员而言基本上是不可见的-除了任何性能方面的好处之外,并且(b)无论如何都不得更改语义。但是,Apple决定最初将其作为“程序员协助的优化”进行介绍,然后逐步改进。
情况2的行为完全取决于将块和__block
变量复制到堆上时的情况。让我们看一下代码:
NSObject * __block blockVar2;
这声明blockVar2
具有__block
存储持续时间,这允许块更改此本地声明的变量的值。在这一点上,编译器不知道某个块是否可以访问它(在这种情况下blockVar2
需要将其放在堆中),或者是否将无法在块中进行访问(在这种情况下它可以位于堆栈上)。
编译器决定优先使用堆栈并在其中进行分配blockVar2
。
blockWithOutPointerThatDispatchesLater(&blockVar2,
现在,编译器需要将地址blockVar2
as作为第一个参数传递,该变量当前在堆栈上,并且编译器发出代码以计算其地址。该地址在堆栈上。
^{
NSLog(@"blockVar2: %@",
blockVar2);
});
现在,编译器进入第二个参数。它看到了该块,该块访问了blockVar2
,并且通过blockVar2
限定了该块,__block
因此它必须捕获变量本身而不是变量值。
编译器决定该块应放在堆上。为此,它需要迁移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实际上是什么。考虑:
在(Objective-)C变量中,在堆栈上或静态分配的变量以及分配的动态内存块在其生存期内均不会移动。
编译器的优化通常不应改变程序的含义或正确性。
苹果公司已经实现了编译器优化-将块和捕获的__block
变量存储在堆栈中-可以在__block
属性变量的生命周期内移动它们,从而可以改变程序的含义和正确性。
结果是对程序语句进行了简单的重新排序,其方式不应该改变程序的含义或事实上的正确性。这不好!
考虑到Apple实施的优化的历史,其设计正确性取决于程序员的帮助(尽管此后已变得更加自动化),以确保其正确性,这可以简单地视为所选实现的另一个“功能”。
推荐
永远不要将address的(&
)运算符应用于具有__block
存储持续时间的变量。如果这样做可能会奏效,但可能并不容易。
如果您需要使用__block
变量作为传递回写参数,则先将其复制到本地临时目录,进行调用,最后再将其复制回。
高温超导
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句