问题描述
以下代码在尝试设置 EXC_BAD_ACCESS
时导致 *error
。
- (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,BOOL *anotherWriteback) {
*error = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil]; // <--- No crash here
}];
}
- (void)runABlock:(void (NS_NOESCAPE ^)(id obj,BOOL *stop))block
{
BOOL anotherWriteback = NO;
block(@"Some string",&anotherWriteback);
}
不确定我是否遗漏了 ARC 在这里的工作原理,或者它是否特定于我使用的 Xcode 版本(Xcode 12.2)。
解决方法
我无法在 -doSetErrorInBlock:
中重现崩溃,但我可以在调试节点中使用“-[NSError retain]: message sent to deallocated instance”重现 -triggerEXC_BAD_ACCESS
中的崩溃(我不是确定是由于 NSZombie 还是其他一些调试选项)。
其原因是 *error
中的 -doSetErrorInBlock:
具有类型 NSError * __autoreleasing
,以及 -[NSArray enumerateObjectsUsingBlock:]
的实现(它是封闭源代码,但可以检查程序集) 恰好在内部有一个围绕块执行的自动释放池。 __autoreleasing
的对象指针意味着我们不保留它,并且我们假设它是活的,因为它被某个自动释放池保留了。这意味着将某些东西分配给自动释放池中的 __autoreleasing
变量,然后在自动释放池结束后尝试访问它是不好的,因为自动释放池的末尾可能已经释放了它,所以你可以离开带有悬空指针。 ARC 规范的 This section 说:
如果一个非空指针被分配给一个未定义的行为
__autoreleasing
对象,而自动释放池在范围内并且
然后在离开自动释放池的范围后读取该对象。
崩溃消息说它正在尝试保留它的原因是因为当您尝试传递“指向 __strong
的指针”时会发生什么(例如 &error
中的 -triggerEXC_BAD_ACCESS
) 到“指向 __autoreleasing
的指针”类型的参数(例如 -doSetErrorInBlock:
的参数)。正如您从 ARC 规范的 this section 中看到的那样,发生了一个“pass-by-writeback”过程,在该过程中,他们创建了一个 __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",&anotherWriteback);
}
}
您真的不应该在您编写的新方法中使用 __autoreleasing
。 __strong
好得多,因为强引用可确保您不会意外地遇到悬空引用和类似问题。 __autoreleasing
存在的主要原因是因为在手动引用计数时代,没有明确的所有权限定符,并且“约定”是保留计数不会传入或传出方法,因此对象从方法(包括使用 out 参数由指针返回的对象)将被自动释放而不是保留。 (并且这些方法将负责确保在方法返回时对象仍然有效。)并且由于您的程序可以在不同的操作系统版本上使用,因此它们无法更改新操作系统版本中 API 的行为,因此它们被困在这个“指向 __autoreleasing
”类型的指针。但是,在您自己用 ARC 编写的方法(它确实具有明确的所有权限定符)中,该方法只能由您自己的 ARC 代码调用,请务必使用 __strong
。如果您使用 __strong
编写方法,它不会崩溃(by default 指向对象的指针被解释为 __autoreleasing
,因此您必须明确指定 __strong
) :
- (void)doSetErrorInBlock:(NSError * __strong *)error
{
[@[@(0)] enumerateObjectsUsingBlock:^(id _Nonnull obj,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,BOOL * _Nonnull stop) {
result = [NSError errorWithDomain:@"some.domain" code:100 userInfo:nil];
}];
*error = result;
}