NSInvocation & NSError - __autoreleasing и сбой памяти

При изучении 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, объявив и назначив адрес **errorPointer. Что немного трудно понять, так это __autoreleasing и управление памятью, что здесь происходит.

Переменная ** errorPointer не является объектом, поэтому она не имеет счетчика сохраняемых данных. Это просто память, которая хранит адрес памяти, который указывает на объект NSError. Я понимаю, что метод stringWith... будет выделять, инициализировать и автоматически освобождать объект NSError и устанавливать *errorPointer = выделенная память. Как вы увидите позже, объект NSError становится недоступным. Это...

  • ... потому что пул авто-релиза истощился?
  • ... потому что ARC заполнил вызов "release" для stringWith...'s 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;
}

Это открыло мне глаза (немного смутило), так как я думал, что знаю, что, черт возьми, я делал, когда дело дошло до управления памятью!

Лучшее, что я мог сделать, чтобы разрешить свой сбой, - это извлечь переменную ошибки NSError * из метода init и сделать ее глобальной. Это потребовало от меня изменить атрибут с __autoreleasing на __strong для **errorPointer. Но очевидно, что это исправление далеко не идеальное, особенно если учесть, что можно многократно использовать NSInvocations в очереди операций. Это также только подтверждает мое подозрение, что * ошибка исправлена.

В качестве финального WTF я попытался немного поиграться с приведениями __bridge, но 1. я не уверен, что мне это здесь нужно, и 2. после перестановки я не смог найти тот, который сработал.

Я хотел бы получить некоторое понимание, которое могло бы помочь мне лучше понять, почему это все не щелкает.

1 ответ

Решение

На самом деле это очень простая ошибка, которая не имеет ничего общего с автоматическим подсчетом ссылок.

В -[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] больше не существует, и попытка чтения из одного приводит к сбою.

Другие вопросы по тегам