Защо ARC миграторът казва, че -setArgument: на NSInvocation не е безопасен, освен ако аргументът не е __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 казва, че използването на силно задържан указател като аргумент за -setArgument: на NSInvocation не е безопасно, освен ако не използвате нещо, което е __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 и времето, когато го използвате, трябва изрично да запазите обектите сами или да извикате метода retainArguments, за да може обектът за извикване да ги запази сам.

person Jingjie Zhan    schedule 26.07.2013