为什么ARC迁移器说NSInvocation的-setArgument:不安全,除非参数是__unsafe_unretained?

时间:2011-12-29 19:46:59

标签: objective-c cocoa automatic-ref-counting

我正在将一段代码迁移到自动引用计数(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'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的东西?

6 个答案:

答案 0 :(得分:9)

这是一个完整的猜测,但可能与参考作为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

答案 1 :(得分:8)

默认情况下,NSInvocation 不保留或复制给定的参数以提高效率,因此作为参数传递的每个对象在调用调用时仍必须存在。这意味着传递给-setArgument:atIndex:的指针将被处理为__unsafe_unretained

您发布的两行MRR代码已经消失了:testNumber1从未发布过。这会导致内存泄漏,但是会有效。但是在ARC中,testNumber1将在其最后一次使用和定义它的块的结尾之间的任何地方释放,因此它将被释放。通过迁移到ARC,代码可能会崩溃,因此ARC迁移工具会阻止您迁移:

  

NSInvocation的setArgument与对象一起使用是不安全的   __unsafe_unretained以外的所有权

简单地将指针作为__unsafe_unretained传递将无法解决问题,您必须确保在调用调用时参数仍然存在。一种方法是像你一样调用-retainArguments(或者甚至更好:直接在创建NSInvocation之后)。然后调用保留其所有参数,因此它保留了被调用所需的所有内容。这可能不那么有效,但它绝对比崩溃更好;)

答案 2 :(得分:2)

  

为什么阻止你这样做?使用__unsafe_unretained对象作为参数似乎同样糟糕。

错误消息可以改进,但迁移器并未说__unsafe_unretained个对象安全NSInvocation一起使用(没有安全使用__unsafe_unretained,它在名称中)。错误的目的是引起你的注意,将强弱/弱对象传递给该API是不安全的,你的代码可能在运行时爆炸,你应该检查代码以确保它不会。

通过使用__unsafe_unretained,您基本上会在代码中引入明确的不安全点,您可以控制并负责所发生的事情。在处理NSInvocation时,在代码中看到这些不安全点是很好的卫生,而不是假设ARC将使用该API正确处理事情。

答案 3 :(得分:1)

在这里完全猜到了。

这可能与调用中存在的retainArguments直接相关。通常,所有方法都描述了它们如何处理直接在参数中使用注释发送给它们的任何参数。这在NSInvocation情况下无效,因为运行时不知道调用对参数的作用。 ARC的目的是尽力保证不泄漏,如果没有这些注释,程序员就可以确认没有泄漏。强迫您使用__unsafe_unretained强迫您执行此操作。

我会把这归结为ARC的一个怪癖(其他包括一些不支持弱引用的东西)。

答案 4 :(得分:1)

这里重要的是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不会为您管理它)。

答案 5 :(得分:0)

根据Apple Doc NSInvocation:

  

默认情况下,此类不保留包含的调用的参数。如果这些对象可能在您创建NSInvocation实例和使用它的时间之间消失,那么您应该自己显式保留对象或调用retainArguments方法以使调用对象自己保留它们。