以下代码在尝试设置*错误
时会导致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 keep]:消息发送到已释放的实例”(我不确定是否是由于 NSZombie 或其他一些调试选项所致)。
原因是*error
在-doSetErrorInBlock:
类型为NSError * __autoreleasing
,并实现-[NSArray enumerateObjectsUsingBlock:]
(这是闭源的,但可以检查程序集)恰好在 block 的执行内部有一个自动释放池。一个对象指针是 __autoreleasing
意味着我们不保留它,并且我们假设它通过被某个自动释放池保留而处于事件状态。这意味着将某些内容分配给 __autoreleasing
是不好的。自动释放池内的变量,然后在自动释放池结束后尝试访问它,因为自动释放池的末尾可能已释放它,因此您可能会留下一个悬空指针。 This section ARC 规范说:
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
中的 -triggerEXC_BAD_ACCESS
)传递给类型参数时会发生什么“指向 __autoreleasing
的指针”(例如 -doSetErrorInBlock:
的参数)。从this section可以看出根据 ARC 规范,会发生“回写传递”过程,其中创建 __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
存在是因为回到手动引用计数时代,没有显式的所有权限定符,并且“约定”是保留计数不会传入或传出方法,因此从方法返回的对象(包括使用指针返回的对象) out-parameter)将被自动释放而不是保留。 (这些方法将负责确保该对象在方法返回时仍然有效。)并且由于您的程序可以在不同的操作系统版本上使用,因此它们无法更改新操作系统版本中 API 的行为,因此它们被困在这个“指向 __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
block 的变量,并且仅将其分配到 __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;
}
关于ios - EXC_BAD_ACCESS 在 enumerateObjectsUsingBlock 中设置传递回写错误,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66318713/