可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I was migrating a block of code to automatic reference counting (ARC), and had the ARC migrator throw the error
NSInvocation's setArgument is not safe to be used with an object with
ownership other than __unsafe_unretained
on code where I had allocated an object using something like
NSDecimalNumber *testNumber1 = [[NSDecimalNumber alloc] initWithString:@"1.0"];
then set it as an NSInvocation argument using
[theInvocation setArgument:&testNumber1 atIndex:2];
Why is it preventing you from doing this? It seems just as bad to use __unsafe_unretained
objects as arguments. For example, the following code causes a crash under 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;
due to the overrelease of testNumber1
, because the temporary __unsafe_unretained
tempNumber
variable is not holding on to it after the original pointer is set to nil
(simulating a case where the invocation is used after the original reference to an argument has gone away). If the -retainArguments
line is uncommented (causing the NSInvocation to hold on to the argument), this code does not crash.
The exact same crash happens if I use testNumber1
directly as an argument to -setArgument:
, and it's also fixed if you use -retainArguments
. Why, then, does the ARC migrator say that using a strongly held pointer as an argument to NSInvocation's -setArgument:
is unsafe, unless you use something that is __unsafe_unretained
?
回答1:
This is a complete guess, but might it be something to do with the argument being passed in by reference as a void*
?
In the case you've mentioned, this doesn't really seem a problem, but if you were to call, eg. getArgument:atIndex:
then the compiler wouldn't have any way of knowing whether the returned argument needed to be retained.
From NSInvocation.h:
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
Given that the compiler doesn't know whether the method will return by reference or not (these two method declarations have identical types and attributes), perhaps the migrator is being (sensibly) cautious and telling you to avoid void pointers to strong pointers?
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
回答2:
An NSInvocation
by default does not retain or copy given arguments for efficiency, so each object passed as argument must still live when the invocation is invoked. That means the pointers passed to -setArgument:atIndex:
are handled as __unsafe_unretained
.
The two lines of MRR code you posted got away with this: testNumber1
was never released. That would have lead to a memory leak, but would have worked. In ARC though, testNumber1
will be released anywhere between its last use and the end of the block in which it is defined, so it will be deallocated. By migrating to ARC, the code may crash, so the ARC migration tool prevents you from migrating:
NSInvocation's setArgument is not safe to be used with an object with
ownership other than __unsafe_unretained
Simply passing the pointer as __unsafe_unretained won't fix the problem, you have to make sure that the argument is still around when the invocation gets called. One way to do this is call -retainArguments
as you did (or even better: directly after creating the NSInvocation
). Then the invocation retains all its arguments, and so it keeps everything needed for being invoked around. That may be not as efficient, but it's definitely preferable to a crash ;)
回答3:
Why is it preventing you from doing this? It seems just as bad to use __unsafe_unretained objects as arguments.
The error message could be improved but the migrator is not saying that __unsafe_unretained
objects are safe to be used with NSInvocation
(there's nothing safe with __unsafe_unretained
, it is in the name). The purpose of the error is to get your attention that passing strong/weak objects to that API is not safe, your code can blow up at runtime, and you should check the code to make sure it won't.
By using __unsafe_unretained
you are basically introducing explicit unsafe points in your code where you are taking control and responsibility of what happens. It is good hygiene to make these unsafe points visible in the code when dealing with NSInvocation
, instead of being under the illusion that ARC will correctly handle things with that API.
回答4:
Throwing in my complete guess here.
This is likely directly related to retainArguments
existing at all on the invocation. In general all methods describe how they will handle any arguments sent to them with annotations directly in the parameter. That can't work in the NSInvocation
case because the runtime doesn't know what the invocation will do with the parameter. ARC's purpose is to do its best to guarantee no leaks, without these annotations it is on the programmer to verify there isn't a leak. By forcing you to use __unsafe_unretained
its forcing you to do this.
I would chalk this up to one of the quirks with ARC (others include some things not supporting weak references).
回答5:
The important thing here is the standard behaviour of NSInvocation:
By default, arguments are not retained and C string arguments are not being copied. Therefore under ARC your code can behave as follows:
// 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];
Therefore the migrator tells you:
At this point you have to explicitly ensure that your object is being retained long enough. Therefore create an unsafe_unretained variable (where you have to keep in mind that ARC won't manage it for you).
回答6:
According to Apple Doc NSInvocation:
This class does not retain the arguments for the contained invocation by default. If those objects might disappear between the time you create your instance of NSInvocation and the time you use it, you should explicitly retain the objects yourself or invoke the retainArguments method to have the invocation object retain them itself.