【问题标题】:Why does the ARC migrator say that NSInvocation's -setArgument: is not safe unless the argument is __unsafe_unretained?为什么 ARC 迁移器说 NSInvocation 的 -setArgument: 不安全,除非参数是 __unsafe_unretained?
【发布时间】:2012-01-30 03:44:10
【问题描述】:

我正在将一段代码迁移到自动引用计数 (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_unretainedtempNumber变量在原来的指针设置为nil后没有持有它(模拟一个在原来的之后使用调用的情况对参数的引用已经消失)。如果 -retainArguments 行未注释(导致 NSInvocation 保留参数),则此代码不会崩溃。

如果我将testNumber1 直接用作-setArgument: 的参数,则会发生完全相同的崩溃,如果您使用-retainArguments,它也会得到修复。那么,为什么 ARC 迁移器会说使用强保持指针作为 NSInvocation 的 -setArgument: 的参数是不安全的,除非您使用 __unsafe_unretained 的东西?

【问题讨论】:

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


    【解决方案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
    

    【讨论】:

    • ARC 迁移器确实检查了 -getArgument:-setArgument: 的强指针,以及 -getReturnValue-setReturnValue: clang.llvm.org/doxygen/TransAPIUses_8cpp_source.html 我可以看到它如何不知道它需要保留参数,但为什么禁止你使用强指针?在这种情况下,为什么不简单地鼓励您使用-retainArguments?我担心的是,当使用强指针时,我的快速小测试项目没有公开一些更微妙的东西。
    • 刚刚添加了一个可能存在问题的案例示例。
    • hmm,好的,所以它明确地在 NSInvocation 上查找 setArgument,而不是因为将 __strong id* 强制转换为 void* 而感到不安。那很有意思。不知道为什么 setArgument 也会成为问题......
    • 有趣的是,ARC 不需要从 __strong id* 到 void* 的显式转换。很容易更改 id 并导致崩溃。
    • 更改为r13581。我猜它被错误地应用于-setArgument:...(它也应该使用const void*)。此外,如果anInvocation 是局部变量,我不确定这两个示例是否安全(除非您使用objc_precise_lifetime 对其进行注释),因为ARC 优化器可能会看到不再使用anInvocation 并在之后立即有效地执行anInvocation = nil;致电-getArgument:...
    【解决方案2】:

    在这里输入我的完整猜测。

    这可能与调用中存在的retainArguments 直接相关。通常,所有方法都描述了它们将如何处理直接在参数中使用注释发送给它们的任何参数。这在NSInvocation 的情况下不起作用,因为运行时不知道调用将对参数做什么。 ARC 的目的是尽最大努力保证没有泄漏,如果没有这些注释,程序员就可以验证没有泄漏。通过强迫你使用__unsafe_unretained 它强迫你这样做。

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

    【讨论】:

    • 我刚刚查看了ARC迁移工具源代码希望得到评论,但它只说它是无效的,没有真正的解释。
    【解决方案3】:

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

    错误消息可以改进,但迁移器并没有说__unsafe_unretained 对象是安全 可以与NSInvocation 一起使用(@987654323 没有任何安全 @,它在名称中)。该错误的目的是让您注意将强/弱对象传递给该 API 是不安全的,您的代码可能会在运行时崩溃,您应该检查代码以确保它不会发生。

    通过使用__unsafe_unretained,您基本上是在代码中引入明确的不安全点,您可以控制和负责所发生的事情。在处理 NSInvocation 时,让这些不安全点在代码中可见是一种很好的卫生习惯,而不是幻想 ARC 会使用该 API 正确处理事情。

    【讨论】:

      【解决方案4】:

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

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

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

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

      【讨论】:

        【解决方案5】:

        根据 Apple Doc NSInvocation:

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

        【讨论】:

          【解决方案6】:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-03
            • 2012-01-25
            • 2014-06-08
            • 2019-12-27
            • 1970-01-01
            相关资源
            最近更新 更多