Почему мигратор ARC говорит, что NSInvocation -setArgument: небезопасно, если аргумент отсутствует __unsafe_unretained?
Я переносил блок кода в автоматический подсчет ссылок (ARC), и когда ARC-мигратор выдавал ошибку
NSInvocation setArgument небезопасно для использования с объектом с собственность, кроме __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 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
?
Ответы
Ответ 1
Это полное предположение, но может быть, это связано с тем, что аргумент передается по ссылке как void*
?
В случае, о котором вы упомянули, это действительно не проблема, но если вы должны позвонить, например. getArgument:atIndex:
, тогда компилятор не имел бы никакого способа узнать, нужно ли сохранить возвращаемый аргумент.
Из NSInvocation.h:
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
Учитывая, что компилятор не знает, будет ли метод возвращаться по ссылке или нет (эти два объявления метода имеют одинаковые типы и атрибуты), возможно, мигрент (разумно) осторожен и говорит вам избегать указателей void для сильных указатели?
Например:
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
Ответ 2
An NSInvocation
по умолчанию не сохраняет или не копирует заданные аргументы для эффективности, поэтому каждый объект, переданный как аргумент, должен оставаться в живых при вызове вызова. Это означает, что указатели, переданные в -setArgument:atIndex:
, обрабатываются как __unsafe_unretained
.
Две строки кода MRR, которые вы опубликовали, ушли с этим: testNumber1
никогда не выпускался. Это привело бы к утечке памяти, но сработало бы. Однако в ARC testNumber1
будет выпущен где угодно между его последним использованием и концом блока, в котором он определен, поэтому он будет освобожден. Перейдя на ARC, код может упасть, поэтому средство миграции ARC не позволяет вам выполнить миграцию:
NSInvocation setArgument небезопасно для использования с объектом с собственность, кроме __unsafe_unretained
Просто передавая указатель, поскольку __unsafe_unretained не устранит проблему, вы должны убедиться, что аргумент все еще существует, когда вызов вызывается. Один из способов сделать это - вызов -retainArguments
, как и вы (или даже лучше: сразу после создания NSInvocation
). Затем вызов сохраняет все свои аргументы, и поэтому он сохраняет все необходимое для вызова. Это может быть не так эффективно, но это определенно предпочтительнее крушения;)
Ответ 3
Почему это мешает вам это делать? Похоже, что использовать __unsafe_unretained объекты в качестве аргументов также не так.
Сообщение об ошибке может быть улучшено, но мигратор не говорит, что объекты __unsafe_unretained
безопасны для использования с NSInvocation
(там нет ничего безопасного с __unsafe_unretained
, оно есть в имени). Цель ошибки - обратить ваше внимание на то, что передача сильных/слабых объектов в этот API небезопасна, ваш код может взорваться во время выполнения, и вы должны проверить код, чтобы убедиться, что он не будет.
Используя __unsafe_unretained
, вы в основном вводите явные небезопасные точки в свой код, где вы берете контроль и ответственность за то, что происходит. Хорошая гигиена делает эти небезопасные точки видимыми в коде при работе с NSInvocation
, а не под иллюзией, что ARC будет правильно обрабатывать вещи с помощью этого API.
Ответ 4
Бросьте в мою полную догадку здесь.
Вероятно, это напрямую связано с retainArguments
, существующим вообще при вызове. В общем, все методы описывают, как они будут обрабатывать любые переданные им аргументы с аннотациями непосредственно в параметре. Это не может работать в случае NSInvocation
, потому что среда выполнения не знает, что вызовет будет делать с параметром. Цель ARC - сделать все возможное, чтобы гарантировать отсутствие утечек, без этих аннотаций на программиста проверить, нет ли утечки. Заставляя вас использовать __unsafe_unretained
, заставляя вас это делать.
Я бы сделал это до одного из причуд с ARC (другие включают некоторые вещи, не поддерживающие слабые ссылки).
Ответ 5
Важным здесь является стандартное поведение 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 не справится с этим для вас).
Ответ 6
Согласно Apple Doc NSInvocation:
Этот класс не сохраняет аргументы для содержащегося вызова по умолчанию. Если эти объекты могут исчезнуть между моментом создания экземпляра NSInvocation и временем его использования, вы должны явно сохранить объекты самостоятельно или вызвать метод saveArguments, чтобы объект-вызов сохранял их сами.