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