EXC_BAD_ACCESS 在 enumerateObjectsUsingBlock 中设置回写传递错误
EXC_BAD_ACCESS from setting pass-by-writeback error within enumerateObjectsUsingBlock
以下代码在尝试设置 *error
时导致 EXC_BAD_ACCESS
。
- (void)triggerEXC_BAD_ACCESS
{
NSError *error = nil;
[self doSetErrorInBlock:&error];
}
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil]; // <--- causes EXC_BAD_ACCESS
}];
}
但是,我不确定为什么会出现 EXC_BAD_ACCESS
。
用以下函数替换 enumerateObjectsUsingBlock:
调用,它试图重现 enumerateObjectsUsingBlock:
的函数签名,将使函数 triggerEXC_BAD_ACCESS
运行 没有错误:
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
[self runABlock:^(id someObject, NSUInteger idx, BOOL *anotherWriteback) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil]; // <--- No crash here
}];
}
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj, NSUInteger idx, BOOL *stop))block
{
BOOL anotherWriteback = NO;
block(@"Some string", 0, &anotherWriteback);
}
不确定我是否遗漏了关于 ARC 在这里如何工作的任何信息,或者它是否特定于我正在使用的 Xcode 版本(Xcode 12.2)。
我无法在 -doSetErrorInBlock:
中重现崩溃,但我可以在 -triggerEXC_BAD_ACCESS
中重现崩溃,并在调试节点中使用“-[NSError retain]: message sent to deallocated instance”(我我不确定是不是因为 NSZombie 或其他一些调试选项。
原因是 -doSetErrorInBlock:
中的 *error
类型为 NSError * __autoreleasing
,-[NSArray enumerateObjectsUsingBlock:]
的实现(它是闭源的,但可以检查程序集)碰巧在内部有一个围绕块执行的自动释放池。一个 __autoreleasing
的对象指针意味着我们不保留它,我们假设它是活着的,因为它被一些自动释放池保留了。这意味着将某些东西分配给自动释放池中的 __autoreleasing
变量,然后在自动释放池结束后尝试访问它是不好的,因为自动释放池的末尾可能已经释放了它,所以你可以离开带有悬空指针。 ARC 规范的 This section 说:
It is undefined behavior if a non-null pointer is assigned to an
__autoreleasing
object while an autorelease pool is in scope and
then that object is read after the autorelease pool’s scope is left.
崩溃消息说它试图保留它的原因是因为当您尝试传递“指向 __strong
”的指针时会发生什么(例如 &error
in -triggerEXC_BAD_ACCESS
) 到类型为“指向 __autoreleasing
的指针”的参数(例如 -doSetErrorInBlock:
的参数)。正如您从 ARC 规范的 this section 中看到的那样,发生了一个“传递回写”过程,他们创建了一个 __autoreleasing
类型的临时变量,分配 __strong
的值变量,进行调用,然后将 __autoreleasing
变量的值分配回 __strong
变量,所以你的 triggerEXC_BAD_ACCESS
方法真的有点像这样:
NSError *error = nil;
NSError * __autoreleasing temporary = error;
[self doSetErrorInBlock:&temporary];
error = temporary;
将值赋回 __strong
变量的最后一步执行保留,那是它遇到释放实例的时候。
如果我将 -runABlock:
更改为:
,我可以在您的第二个示例中重现相同的崩溃
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj, NSUInteger idx, BOOL *stop))block
{
BOOL anotherWriteback = NO;
@autoreleasepool {
block(@"Some string", 0, &anotherWriteback);
}
}
您不应该真正在您编写的新方法中使用 __autoreleasing
。 __strong
好多了,因为强引用确保你不会意外地有悬空引用和类似的问题。 __autoreleasing
存在的主要原因是因为在手动引用计数时代,没有明确的所有权限定符,并且“约定”是保留计数不会转移到方法中或从方法中转移出来,因此对象从方法(包括使用输出参数的指针返回的对象)将被自动释放而不是保留。 (并且这些方法将负责确保对象在方法 returns 时仍然有效。)并且由于您的程序可以在不同的 OS 版本上使用,因此它们无法更改新 API 的行为OS 版本,所以他们被这种“指向 __autoreleasing
的指针”类型所困。但是,在您自己在 ARC 中编写的方法中(它确实具有明确的所有权限定符),它只会被您自己的 ARC 代码调用,一定要使用 __strong
。如果您使用 __strong
编写方法,它不会崩溃(by default 指向对象指针的指针被解释为 __autoreleasing
,因此您必须明确指定 __strong
) :
- (void)doSetErrorInBlock:(NSError * __strong *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
}
如果您出于某种原因坚持使用类型为 NSError * __autoreleasing *
的参数,并且想要安全地做您正在做的相同事情,您应该为块使用 __strong
变量, 只赋值到最后的 __autoreleasing
:
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
__block NSError *result;
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
result = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
*error = result;
}
以下代码在尝试设置 *error
时导致 EXC_BAD_ACCESS
。
- (void)triggerEXC_BAD_ACCESS
{
NSError *error = nil;
[self doSetErrorInBlock:&error];
}
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil]; // <--- causes EXC_BAD_ACCESS
}];
}
但是,我不确定为什么会出现 EXC_BAD_ACCESS
。
用以下函数替换 enumerateObjectsUsingBlock:
调用,它试图重现 enumerateObjectsUsingBlock:
的函数签名,将使函数 triggerEXC_BAD_ACCESS
运行 没有错误:
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
[self runABlock:^(id someObject, NSUInteger idx, BOOL *anotherWriteback) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil]; // <--- No crash here
}];
}
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj, NSUInteger idx, BOOL *stop))block
{
BOOL anotherWriteback = NO;
block(@"Some string", 0, &anotherWriteback);
}
不确定我是否遗漏了关于 ARC 在这里如何工作的任何信息,或者它是否特定于我正在使用的 Xcode 版本(Xcode 12.2)。
我无法在 -doSetErrorInBlock:
中重现崩溃,但我可以在 -triggerEXC_BAD_ACCESS
中重现崩溃,并在调试节点中使用“-[NSError retain]: message sent to deallocated instance”(我我不确定是不是因为 NSZombie 或其他一些调试选项。
原因是 -doSetErrorInBlock:
中的 *error
类型为 NSError * __autoreleasing
,-[NSArray enumerateObjectsUsingBlock:]
的实现(它是闭源的,但可以检查程序集)碰巧在内部有一个围绕块执行的自动释放池。一个 __autoreleasing
的对象指针意味着我们不保留它,我们假设它是活着的,因为它被一些自动释放池保留了。这意味着将某些东西分配给自动释放池中的 __autoreleasing
变量,然后在自动释放池结束后尝试访问它是不好的,因为自动释放池的末尾可能已经释放了它,所以你可以离开带有悬空指针。 ARC 规范的 This section 说:
It is undefined behavior if a non-null pointer is assigned to an
__autoreleasing
object while an autorelease pool is in scope and then that object is read after the autorelease pool’s scope is left.
崩溃消息说它试图保留它的原因是因为当您尝试传递“指向 __strong
”的指针时会发生什么(例如 &error
in -triggerEXC_BAD_ACCESS
) 到类型为“指向 __autoreleasing
的指针”的参数(例如 -doSetErrorInBlock:
的参数)。正如您从 ARC 规范的 this section 中看到的那样,发生了一个“传递回写”过程,他们创建了一个 __autoreleasing
类型的临时变量,分配 __strong
的值变量,进行调用,然后将 __autoreleasing
变量的值分配回 __strong
变量,所以你的 triggerEXC_BAD_ACCESS
方法真的有点像这样:
NSError *error = nil;
NSError * __autoreleasing temporary = error;
[self doSetErrorInBlock:&temporary];
error = temporary;
将值赋回 __strong
变量的最后一步执行保留,那是它遇到释放实例的时候。
如果我将 -runABlock:
更改为:
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj, NSUInteger idx, BOOL *stop))block
{
BOOL anotherWriteback = NO;
@autoreleasepool {
block(@"Some string", 0, &anotherWriteback);
}
}
您不应该真正在您编写的新方法中使用 __autoreleasing
。 __strong
好多了,因为强引用确保你不会意外地有悬空引用和类似的问题。 __autoreleasing
存在的主要原因是因为在手动引用计数时代,没有明确的所有权限定符,并且“约定”是保留计数不会转移到方法中或从方法中转移出来,因此对象从方法(包括使用输出参数的指针返回的对象)将被自动释放而不是保留。 (并且这些方法将负责确保对象在方法 returns 时仍然有效。)并且由于您的程序可以在不同的 OS 版本上使用,因此它们无法更改新 API 的行为OS 版本,所以他们被这种“指向 __autoreleasing
的指针”类型所困。但是,在您自己在 ARC 中编写的方法中(它确实具有明确的所有权限定符),它只会被您自己的 ARC 代码调用,一定要使用 __strong
。如果您使用 __strong
编写方法,它不会崩溃(by default 指向对象指针的指针被解释为 __autoreleasing
,因此您必须明确指定 __strong
) :
- (void)doSetErrorInBlock:(NSError * __strong *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
}
如果您出于某种原因坚持使用类型为 NSError * __autoreleasing *
的参数,并且想要安全地做您正在做的相同事情,您应该为块使用 __strong
变量, 只赋值到最后的 __autoreleasing
:
- (void)doSetErrorInBlock:(NSError * __autoreleasing *)error
{
__block NSError *result;
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
result = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
*error = result;
}