NSInvocation & NSError - __autoreleasing & memory

2019-04-10 11:47发布

In learning about NSInvocations it seems like I've got a gap in my understanding about memory management.

Here is a sample project:

@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

Of course, all I'm doing here is building up an invocation object as a property for the NSString class method

+[NSString stringWithContentsOfFile:(NSString \*)path encoding:(NSStringEncoding)enc error:(NSError \**)error]

It makes sense, especially after reading this blog post, as to why I need to handle the NSError object by declaring and assigning the address to **errorPointer. What is a little difficult to grasp is the __autoreleasing and memory management what is happening here.

The **errorPointer variable isn't an object, so it does not have a retain count. It simply is memory that stores a memory address which points to an NSError object. I understand that the stringWith... method will alloc, init, and autorelease an NSError object, and set the *errorPointer = the allocated memory. As you'll see later, the NSError object becomes inaccessible. Is this...

  • ...because an autorelease pool has drained?
  • ...because ARC filled in the "release" call to stringWith...'s alloc + init?

So let's take a look at how the invocation "works"

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

This has been eye opening (a bit disconcerting) for me, as I thought I knew what the hell I was doing when it came to memory management!

The best I could do to resolve my crasher, is to pull the NSError *error variable out from the init method, and make it global. This required me to change the attribute from __autoreleasing to __strong on **errorPointer. But, clearly that fix is less than ideal, especially given that one is likely to reuse NSInvocations many times in an operation queue. It also only kinda confirms my suspicion that *error is being dealloc'd.

As a final WTF, I tried played around a bit with the __bridge casts, but 1. I'm not sure if that's what I need here and 2. after permuting I couldn't find one that worked.

I'd love some insight that might help me better understand why this all isn't clicking.

1条回答
Juvenile、少年°
2楼-- · 2019-04-10 12:35

This is actually a very simple error that has nothing to do with Automatic Reference Counting.

In -[DoNothing init], you're initializing the error parameter of the invocation with a pointer to a stack variable:

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

And in main, you're grabbing that same pointer and dereferencing it:

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

But of course by this point all the local variables that live in -[DoNothing init] no longer exist, and attempting to read from one produces a crash.

查看更多
登录 后发表回答