NSInvocation和NSError - __autoreleasing&内存崩溃问题

5

在学习关于NSInvocations的内容时,我发现自己对内存管理存在一些理解上的差距。

以下是一个示例项目:

@interface DoNothing : NSObject
@property (nonatomic, strong) NSInvocation *invocation;
@end

@implementation DoNothing
@synthesize invocation = _invocation;

NSString *path = @"/Volumes/Macintosh HD/Users/developer/Desktop/string.txt";

- (id)init
{
    self = [super init];
    if (self) {

        SEL selector = @selector(stringWithContentsOfFile:encoding:error:);
        NSInvocation *i = [NSInvocation invocationWithMethodSignature:[NSString methodSignatureForSelector:selector]];

        Class target = [NSString class];
        [i setTarget:target];
        [i setSelector:@selector(stringWithContentsOfFile:encoding:error:)];

        [i setArgument:&path atIndex:2];

        NSStringEncoding enc = NSASCIIStringEncoding;
        [i setArgument:&enc atIndex:3];

        __autoreleasing NSError *error;
        __autoreleasing NSError **errorPointer = &error;
        [i setArgument:&errorPointer atIndex:4];

        // I understand that I need to declare an *error in order to make sure
        // that **errorPointer points to valid memory. But, I am fuzzy on the
        // __autoreleasing aspect. Using __strong doesn't prevent a crasher.

        [self setInvocation:i];
    }

    return self;
}

@end

当然,我在这里做的只是为NSString类方法构建调用对象作为属性。
+[NSString stringWithContentsOfFile:(NSString \*)path encoding:(NSStringEncoding)enc error:(NSError \**)error]

在阅读了这篇博客文章之后,我意识到需要通过声明和分配地址来处理NSError对象。然而,在这里发生的__autoreleasing和内存管理是有些难以理解的。

**errorPointer变量不是一个对象,所以它没有保留计数。它只是存储指向NSError对象的内存地址的内存。我理解stringWith...方法将分配、初始化并自动释放NSError对象,并设置*errorPointer=分配的内存。正如以后会看到的,NSError对象变得无法访问。这是因为...

  • ...自动释放池已经被排空?
  • ...ARC填写了stringWith...的alloc+init中的"release"调用?

因此,让我们来看看这个调用是如何“工作”的。

int main(int argc, const char * argv[])
{
    @autoreleasepool {

        NSError *regularError = nil;
        NSString *aReturn = [NSString stringWithContentsOfFile:path
                                                      encoding:NSASCIIStringEncoding
                                                         error:&regularError];

        NSLog(@"%@", aReturn);

        DoNothing *thing = [[DoNothing alloc] init];
        NSInvocation *invocation = [thing invocation];

        [invocation invoke];

        __strong NSError **getErrorPointer;
        [invocation getArgument:&getErrorPointer atIndex:4];
        __strong NSError *getError = *getErrorPointer;  // CRASH! EXC_BAD_ACCESS

        // It doesn't really matter what kind of attribute I set on the NSError
        // variables; it crashes. This leads me to believe that the NSError
        // object that is pointed to is being deallocated (and inspecting with
        // NSZombies on, confirms this).

        NSString *bReturn;
        [invocation getReturnValue:&bReturn];
    }
    return 0;
}

这对我来说是一个启示(有点令人不安),因为我以为我在内存管理方面知道该怎么做!
为了解决我的崩溃问题,我所能做的最好的办法是从init方法中取出NSError *error变量,并将其设置为全局变量。这要求我将**errorPointer上的属性从__autoreleasing更改为__strong。但很明显,这种修复方式并不理想,特别是考虑到在操作队列中可能会多次重用NSInvocation。此外,它只是“有点”证实了我的猜测,即*error已被dealloc。
作为最后的困惑,我尝试了一些__bridge转换,但是:1.我不确定这是否是我需要的,2.在排列组合之后我找不到有效的转换方法。
我希望有一些见解能帮助我更好地理解为什么所有这些东西都不能奏效。
1个回答

7
这实际上是一个非常简单的错误,与自动引用计数无关。在- [DoNothing init]中,你正在使用指向堆栈变量的指针初始化调用的错误参数:
__autoreleasing NSError *error;
__autoreleasing NSError **errorPointer = &error;
[i setArgument:&errorPointer atIndex:4];

main 函数中,你正在获取相同的指针并对其进行解引用:

__strong NSError **getErrorPointer;
[invocation getArgument:&getErrorPointer atIndex:4];
__strong NSError *getError = *getErrorPointer;

但是很显然,到这个点上,在 -[DoNothing init] 中存在的所有局部变量都不再存在,试图从其中读取一个变量会导致崩溃。


明白了!所以,我应该将NSError *error声明为__strong static NSError *error(使它成为堆变量?这样说合理吗?我认为栈/堆是实现细节...) - edelaney05
啊,不要把它设为静态的。只是不要把NSInvocation作为你的类的公共部分。这个错误之所以出现是因为在它不再有用之后仍然可以访问它。如果你一定要这样做,那就把错误变量设为你的类的属性。(顺便说一下,堆栈和堆不是实现细节,它们是基本的对象生命周期概念。) - John Calsbeek
错误参数仅在调用触发后才有用。您可能会将调用放在计时器或操作队列中,因此错误变量几乎肯定会超出范围。但是,现在对于为什么它崩溃的一般意义已经非常清楚了 - 谢谢! - edelaney05
1
@edelaney05 啊,好吧,如果整个对象都是围绕着一个 NSInvocation 的包装器,那么让一个错误变量与其一起运行就有意义了,并且这将解决生命周期问题。我撤回我的反对。 - John Calsbeek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接