2012-04-03 33 views
4

在学习NSInvocations时,似乎我在理解内存管理方面存在差距。NSInvocation&NSError - __autoreleasing&memory crasher

下面是一个简单的项目:

@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] 

属性这是有道理的,特别是在阅读this blog post之后,至于为什么我需要通过声明并将地址分配给** errorPointer来处理NSError对象。有一点难以理解的是,这里发生的是__autoreleasing和内存管理。

** errorPointer变量不是一个对象,所以它没有保留计数。它只是内存中存储指向NSError对象的内存地址。我知道stringWith ...方法会分配init,并自动释放一个NSError对象,并设置* errorPointer =分配的内存。正如你稍后会看到的,NSError对象变得不可访问。这是...

  • ...因为autorelease池已经耗尽?
  • ...因为ARC在“释放”调用stringWith ...的alloc + init?

那么让我们来看看调用如何“工作”

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 *错误变量,并使其全局。这要求我在** errorPointer上将属性从__autoreleasing更改为__strong。但是,显然这种修复并不理想,特别是考虑到可能在操作队列中多次重复使用NSInvocations。它也只有kinda证实我怀疑*错误正在被解除分配。

作为一个最终的WTF,我试着玩了一下__bridge casts,但是1.我不确定这是我在这里所需要的,2.在排除后我找不到一个能工作的人。

我喜欢一些洞察力,可能会帮助我更好地理解为什么这一切都不是点击。

回答

6

这实际上是一个非常简单的错误,与自动引用计数无关。

-[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]中的所有局部变量不再存在,试图从其中读取会产生崩溃。

+0

Gotcha!所以真的,我应该宣布NSError *错误为'__strong静态NSError *错误'(使它成为一个堆变量?这是甚至合理的说法?我认为堆栈/堆是一个实现细节...) – edelaney05 2012-04-04 01:23:42

+0

啊, ,不要让它变成静态的。只是不要让'NSInvocation'成为你的类的公共部分。该错误只是因为你可以在停止使用后访问它。如果您*必须*,请将错误变量作为您班级的一个属性。 (顺便说一下,堆栈与堆不是实现细节,它们是基本的对象生命周期概念。) – 2012-04-04 01:39:56

+0

错误参数仅在*调用被触发后有用*。您可能会在计时器或操作队列中附加调用,所以错误变量几乎肯定会超出范围。但是,它为什么崩溃的总体要点现在已经很清楚了 - 谢谢! – edelaney05 2012-04-04 02:40:25