Почему средство миграции ARC говорит, что NSInvocation -setArgument: небезопасен, если аргумент не является __unsafe_unretained?

Я переносил блок кода на автоматический подсчет ссылок (ARC), и средство миграции ARC выдало ошибку

SetArgument NSInvocation небезопасен для использования с объектом с другим владельцем, кроме __unsafe_unretained

в коде, где я выделил объект, используя что-то вроде

NSDecimalNumber *testNumber1 = [[NSDecimalNumber alloc] initWithString:@"1.0"];

затем установите его как аргумент NSInvocation, используя

[theInvocation setArgument:&testNumber1 atIndex:2];

Почему оно мешает вам это сделать? Плохо использовать объекты __unsafe_unretained в качестве аргументов. Например, следующий код вызывает сбой в ARC:

NSDecimalNumber *testNumber1 = [[NSDecimalNumber alloc] initWithString:@"1.0"];
NSMutableArray *testArray = [[NSMutableArray alloc] init];

__unsafe_unretained NSDecimalNumber *tempNumber = testNumber1;

NSLog(@"Array count before invocation: %ld", [testArray count]);
//    [testArray addObject:testNumber1];    
SEL theSelector = @selector(addObject:);
NSMethodSignature *sig = [testArray methodSignatureForSelector:theSelector];
NSInvocation *theInvocation = [NSInvocation invocationWithMethodSignature:sig];
[theInvocation setTarget:testArray];
[theInvocation setSelector:theSelector];
[theInvocation setArgument:&tempNumber atIndex:2];
//        [theInvocation retainArguments];

// Let's say we don't use this invocation until after the original pointer is gone
testNumber1 = nil;

[theInvocation invoke];
theInvocation = nil;

NSLog(@"Array count after invocation: %ld", [testArray count]);
testArray = nil;

из-за перевыпуска testNumber1, потому что временная переменная __unsafe_unretained tempNumber не удерживает ее после того, как исходный указатель установлен на nil (имитация случая, когда вызов используется после того, как исходная ссылка на аргумент исчезла). Если строка -retainArguments не закомментирована (что приводит к тому, что NSInvocation удерживает аргумент), этот код не дает сбоя.

Точно такой же сбой происходит, если я использую testNumber1 напрямую в качестве аргумента для -setArgument:, и это также исправлено, если вы используете -retainArguments. Почему же тогда средство миграции ARC говорит, что использование сильно удерживаемого указателя в качестве аргумента для NSInvocation -setArgument: небезопасно, если вы не используете что-то, что является __unsafe_unretained?


person Brad Larson    schedule 29.12.2011    source источник


Ответы (6)


Это полное предположение, но может ли это быть как-то связано с тем, что аргумент передается по ссылке как void*?

В случае, который вы упомянули, это не кажется проблемой, но если бы вы позвонили, например. getArgument:atIndex:, то компилятор не сможет узнать, нужно ли сохранять возвращенный аргумент.

Из NSInvocation.h:

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

Учитывая, что компилятор не знает, будет ли метод возвращаться по ссылке или нет (объявления этих двух методов имеют идентичные типы и атрибуты), возможно, средство миграции (разумно) осторожничает и говорит вам избегать пустых указателей на сильные указатели?

Eg:

NSDecimalNumber* val;
[anInvocation getArgument:&val atIndex:2];
anInvocation = nil;
NSLog(@"%@", val); // kaboom!


__unsafe_unretained NSDecimalNumber* tempVal;
[anInvocation getArgument:&tempVal atIndex:2];
NSDecimalNumber* val = tempVal;
anInvocation = nil;
NSLog(@"%@", val); // fine
person Chris Devereux    schedule 29.12.2011
comment
Мигратор ARC проверяет -getArgument: и -setArgument: на наличие надежных указателей, а также -getReturnValue и -setReturnValue: clang.llvm .org/doxygen/TransAPIUses_8cpp_source.html Я понимаю, как он не знает, что ему нужно сохранять аргументы, но почему запрещать вам использовать надежные указатели? Почему бы просто не предложить вам использовать -retainArguments в этом случае? Меня беспокоит то, что при использовании надежных указателей есть что-то более тонкое, чего не раскрывает мой быстрый небольшой тестовый проект. - person Brad Larson; 30.12.2011
comment
Просто добавил пример того случая, когда это может быть проблемой. - person Chris Devereux; 30.12.2011
comment
хм, хорошо, так что он явно ищет setArgument в NSInvocation, а не расстраивается из-за приведения __strong id* к void*. Это интересно. Не уверен, почему setArgument тоже будет проблемой... - person Chris Devereux; 30.12.2011
comment
Интересно, что ARC не требует явного приведения __strong id* к void*. Легко изменить идентификатор и вызвать сбой. - person Firoze Lafeer; 30.12.2011
comment
Это изменение r13581. Я предполагаю, что это ошибочно применено к -setArgument:... (тоже должно быть const void*). Кроме того, я не уверен, что любой из примеров безопасен, если anInvocation является локальной переменной (если вы не аннотируете ее с помощью objc_precise_lifetime), потому что оптимизатор ARC может увидеть, что anInvocation больше не используется, и эффективно выполнить anInvocation = nil; сразу после вызова -getArgument:.... - person tc.; 11.05.2012

NSInvocation по умолчанию не сохраняет и не копирует заданные аргументы для повышения эффективности, поэтому каждый объект, переданный в качестве аргумента, должен оставаться активным при вызове вызова. Это означает, что указатели, переданные в -setArgument:atIndex:, обрабатываются как __unsafe_unretained.

Две строки кода MRR, которые вы разместили, сошли с рук: testNumber1 никогда не выпускался. Это привело бы к утечке памяти, но сработало бы. Однако в ARC testNumber1 будет освобожден в любом месте между его последним использованием и концом блока, в котором он определен, поэтому он будет освобожден. При переходе на ARC может произойти сбой кода, поэтому инструмент миграции ARC не позволит вам выполнить миграцию:

SetArgument NSInvocation небезопасен для использования с объектом с другим владельцем, кроме __unsafe_unretained

Простая передача указателя как __unsafe_unretained не решит проблему, вы должны убедиться, что аргумент все еще существует, когда вызывается вызов. Один из способов сделать это — вызвать -retainArguments, как вы это сделали (или даже лучше: сразу после создания NSInvocation). Затем вызов сохраняет все свои аргументы, и поэтому он сохраняет все необходимое для вызова. Это может быть не так эффективно, но это определенно предпочтительнее, чем сбой;)

person Tammo Freese    schedule 25.03.2013

Почему оно мешает вам это сделать? Кажется, так же плохо использовать __unsafe_unretained объекты в качестве аргументов.

Сообщение об ошибке можно было бы улучшить, но средство миграции не говорит, что объекты __unsafe_unretained безопасны для использования с NSInvocation__unsafe_unretained нет ничего безопасного, это указано в названии). Цель ошибки — привлечь ваше внимание к тому, что передача сильных/слабых объектов в этот API небезопасна, ваш код может взорваться во время выполнения, и вы должны проверить код, чтобы убедиться, что этого не произойдет.

Используя __unsafe_unretained, вы вводите явные небезопасные точки в свой код, где вы берете на себя контроль и ответственность за происходящее. Это хорошая гигиена, чтобы сделать эти небезопасные точки видимыми в коде при работе с NSInvocation, вместо того, чтобы питать иллюзию, что ARC будет правильно обрабатывать вещи с этим API.

person aky    schedule 07.07.2012

Бросив в мое полное предположение здесь.

Вероятно, это напрямую связано с тем, что retainArguments вообще существует при вызове. В общем, все методы описывают, как они будут обрабатывать любые отправленные им аргументы с аннотациями прямо в параметре. Это не может работать в случае NSInvocation, потому что среда выполнения не знает, что вызов будет делать с параметром. Цель ARC — сделать все возможное, чтобы гарантировать отсутствие утечек, без этих аннотаций программист должен убедиться, что утечки нет. Заставляя вас использовать __unsafe_unretained, вы заставляете вас делать это.

Я бы списал это на одну из особенностей ARC (другие включают в себя некоторые вещи, не поддерживающие слабые ссылки).

person Joshua Weinberg    schedule 29.12.2011
comment
Я только что проверил исходный код инструмента миграции ARC в надежде на комментарий, но все, что он сказал, это то, что он недействителен, без реального объяснения. - person Joshua Weinberg; 30.12.2011

Здесь важно стандартное поведение NSInvocation: по умолчанию аргументы не сохраняются и аргументы строки C не копируются. Поэтому в ARC ваш код может вести себя следующим образом:

// Creating the testNumber
NSDecimalNumber *testNumber1 = [[NSDecimalNumber alloc] initWithString:@"1.0"];

// Set the number as argument
[theInvocation setArgument:&testNumber1 atIndex:2];

// At this point ARC can/will deallocate testNumber1, 
// since NSInvocation does not retain the argument
// and we don't reference testNumber1 anymore

// Calling the retainArguments method happens too late.
[theInvocation retainArguments];

// This will most likely result in a bad access since the invocation references an invalid pointer or nil
[theInvocation invoke];

Поэтому средство миграции говорит вам: на этом этапе вы должны явно убедиться, что ваш объект сохраняется достаточно долго. Поэтому создайте переменную unsafe_unretained (где вы должны помнить, что ARC не будет управлять ею за вас).

person Nostradani    schedule 15.05.2014

Согласно Apple Doc NSInvocation:

Этот класс по умолчанию не сохраняет аргументы для содержащегося вызова. Если эти объекты могут исчезнуть в период между созданием экземпляра NSInvocation и временем его использования, вы должны явно сохранить объекты самостоятельно или вызвать метод continueArguments, чтобы объект вызова сохранил их сам.

person Jingjie Zhan    schedule 26.07.2013