【问题标题】:Is it safe to pass an argument to a selector that doesn't accept any?将参数传递给不接受任何参数的选择器是否安全?
【发布时间】:2017-09-22 12:42:49
【问题描述】:

我开始学习 Objective-C,并且想知道如果将对象传递给对方法的动态调用会发生什么,而该方法不接受任何方法。

#import <Foundation/Foundation.h>

# pragma mark Forward declarations 

@class DynamicWorker;
@class DynamicExecutor;

# pragma mark Interfaces

// Protocol for a worker object, not receiving any parameters
@protocol Worker<NSObject>

-(void)doStuff;

@end

// Dynamic worker returns a selector to a private method capable of
// doing work.
@interface DynamicWorker : NSObject<Worker>

- (SEL)getWorkDynamically;

@end

// Dynamic executor calls a worker with a selector it provided. The
// executor passes itself, in case the worker needs to launch more 
// workers. The method signature should be of the form
//    (void)method:(DynamicExecutor *)executor
@interface DynamicExecutor : NSObject

- (void)runWorker:(id<Worker>)worker withSelector:(SEL)selector;

@end

#pragma mark Implementations

@implementation DynamicWorker;

- (SEL)getWorkDynamically {
    return @selector(doStuff);
}

-(void) doStuff {
    NSLog(@"I don't accept parameters");
}

@end

@implementation DynamicExecutor;

// Here I get a warning, that I choose to ignore now:
// https://*.com/q/7017281/946814
- (void)runWorker:(id<Worker>)worker withSelector:(SEL)selector {
    [worker performSelector:selector withObject:self];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
      NSLog(@"Getting busy");

      DynamicWorker *worker = [[DynamicWorker alloc] init];
      DynamicExecutor *executor = [[DynamicExecutor alloc] init];
      [executor runWorker:worker withSelector:[worker getWorkDynamically]];
    }
    return 0;
}

到目前为止,它似乎没有引起任何问题,实际上看起来类似于 Javascript 事件处理程序,其中接受事件是可选的。不过,根据我对裸机的理解,我相信参数会放在堆栈上,并且不知道运行时如何知道它应该被丢弃。

【问题讨论】:

    标签: objective-c selector


    【解决方案1】:

    不过,根据我对裸机的理解,我相信参数会放在堆栈上,并且不知道运行时如何知道它应该被丢弃。

    你是对的,调用者将参数放在堆栈上。调用返回后,调用者删除它放置在堆栈中的参数,因此丢弃 被调用者 不期望的任何额外参数不是问题。

    然而,仅仅知道你的代码是否可以工作还不够,被调用者需要知道参数在堆栈中的位置。堆栈通常会随着项目被压入堆栈而向下增长,并且被调用者将参数定位为堆栈指针的正偏移量。如果参数从左到右推送,则最后一个参数位于堆栈指针的最小偏移量处,第一个参数位于最大偏移量处。如果在这种情况下推送额外的参数,那么预期参数的偏移量将全部改变。但是(Objective-)C 支持可变参数函数,即那些接受未指定数量参数的函数(想想printfstringWithFormat: 等),因此调用中的参数从右向左推送,at至少对于可变参数函数,因此第一个参数是最后推送的,因此无论推送多少参数,都与堆栈指针有一个已知的常量偏移量。

    最后,Objective-C 方法调用被转换为对运行时函数objc_msgSend() 的调用,该函数实现了动态方法查找。该函数是可变参数的(因为不同的消息采用不同数量的参数)。

    所以你的 Objective-C 方法调用变成了对可变参数运行时函数的调用,如果你提供了太多参数,它们会被 callee 忽略并被 caller 清除em>。

    希望这一切都有意义!

    附录

    在 cmets 中 @newacct 已经正确指出 objc_msgSend 不是可变参数;我应该写“有效可变参数”,因为我为了简单起见模糊了细节。他们还认为这是一个“蹦床”,而不是一个功能。虽然这在技术上是正确的,但蹦床本质上是一个跳转到其他代码而不是直接返回的函数,其他代码执行返回给调用者(这类似于尾调用优化所做的)。

    回到“本质上是可变的”:objc_msgSend 函数,像所有实现 Objective-C 方法的函数一样,接受第一个参数,这是调用该方法的对象引用,第二个参数是选择器所需的方法,然后按该方法采用的任何参数的顺序 - 因此调用采用可变数量的参数,但严格来说不是可变参数函数

    要定位在运行时调用的实际方法实现objc_msgSend 使用前两个参数;对象引用和选择器;并执行查找。当它找到适当的实现时,它会跳转/尾调用/蹦床。由于objc_msgSend 在检查选择器(即第二个参数)之前无法知道传递了多少个参数,因此它需要能够将第二个参数定位在距堆栈指针已知的偏移量处,并且为此(很容易) 可能的参数必须以相反的顺序推送 - 就像使用可变参数函数一样。由于调用者以相反的顺序推送参数,因此它们对被调用者没有影响,并且其他参数将被忽略且无害提供调用者负责在调用后删除参数。

    对于可变参数函数,调用者必须是删除参数的那个,因为只有它知道传递了多少参数,对于非可变参数函数,被调用者可以删除参数 - 这包括objc_msgSend tail 调用的被调用者- 但许多编译器,包括 Clang,都会让调用者删除它们。

    因此,对 objc_msgSend 的调用(即方法调用的编译)在 Clang 下将通过与可变参数函数基本相同的机制忽略任何额外参数。

    希望它更清楚,不会增加混乱!

    (注意:实际上有些参数可能在寄存器中而不是在堆栈中传递,这对上面的描述没有太大影响。)

    【讨论】:

    • “这个函数是可变的(因为不同的消息采用不同数量的参数)。” objc_msgSend 不是可变参数函数,尝试将参数传递给它,就好像它是可变参数函数一样并不总是有效。相反,objc_msgSend 是一个蹦床,它充当与调用的实现函数相同的函数类型,并且要正确使用objc_msgSend,您需要首先将其转换为适当的实现函数类型,然后将适当的参数传递给结果表达式,以便编译器正确构造调用。
    • 虽然Objective-C支持可变参数函数,但这无关紧要,因为这里的函数不是可变参数,可变参数和非可变参数的ABI可能不同。您认为它适用于可变参数函数的论点并不意味着它适用于非可变参数函数。 C 有可变参数函数,但如果你试图用错误类型的函数指针调用函数(例如,错误的参数数量或类型,或错误的返回类型),这是未定义的行为。
    • @newacct - 我可能应该在答案中抛出一个“基本上”或两个 - 有时模糊边缘会有所帮助,但您显然认为答案是错误的。您能否添加您认为正确的答案?谢谢。
    • @newacct - 你还没有其他答案,所以我添加了一个附录,解释了我模糊不清的细节,因为行为本质上与可变参数函数相同。希望可以解决问题,但如果没有,请添加您自己的答案!
    【解决方案2】:

    您正在调用方法-[NSObject performSelector:withObject:],其documentation

    aSelector 应该标识一个采用单个参数的方法 输入id

    那么你违反了 API 的约定。

    如果您查看 Objective-C 运行时中的 -[NSObject performSelector:]-[NSObject performSelector:withObject:]-[NSObject performSelector:withObject:withObject:] 方法中的 source code,它们都只是 objc_msgSend 的简单包装器——它们每个都转换为 @ 987654330@ 到具有适当数量的id 参数的方法的实现将具有的函数类型。他们能够执行这些转换,因为他们假设您传递的选择器对应于具有适当数量的 id 参数的函数,如文档中所述。

    当您调用objc_msgSend 时,您必须调用它就好像它具有被调用方法的底层实现函数的类型。这是因为objc_msgSend 是一个用汇编语言编写的蹦床,它调用实现函数的所有寄存器和堆栈空间的参数与调用objc_msgSend 时完全相同,因此调用者必须完全按照预期设置参数被调用者(底层实现函数)。这样做的方法是将objc_msgSend 转换为方法的实现函数将具有的函数指针类型(考虑其参数和返回类型),然后使用它进行调用。

    对于所有效果和目的,我们可以认为对objc_msgSend 的调用与直接对底层实现函数的调用相同(即,我们可以将((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj) 视为与((id(*)(id, SEL, id))[self methodForSelector:sel])(self, sel, obj) 相同)。因此,将performSelector:withObject: 与具有较少参数的方法一起使用的问题基本上可以归结为:使用具有比函数实际参数更多的类型的函数指针调用 C 函数是否安全(即函数指针类型具有函数具有的所有参数,具有相同的类型,但最后有额外的参数)

    根据 C 标准,对此的一般答案是,不,使用不同类型的函数指针调用函数是未定义的行为。例如,参见 C99 标准第 6.5.2.2 节第 9 段:

    如果函数定义的类型与 表示表达式所指向的(表达式的)类型 调用函数,行为未定义。

    但是,对于所有使用 Objective-C 的平台(32 位和 64 位 x86;32 位和 64 位 ARM),我相信函数调用约定是这样的 调用者可以安全地使用比被调用者预期的更多的参数来设置函数调用,并且额外传递的参数将被简单地忽略(被调用者不会知道它们在那里,但这没有任何负面影响效果;即使被调用者将寄存器和堆栈空间用于其他事情的那些额外参数,也允许被调用者这样做)。我没有详细检查 ABI,但我相信这是真的。

    但是,如果将 Objective-C 移植到新平台,您将需要检查该平台的函数调用约定,以确定调用者使用比被调用者预期更多的参数进行调用是否会在该平台上导致任何问题.您不能只是假设它适用于所有平台。

    【讨论】:

    • 知道我们依赖于未定义的行为是一个非常受欢迎的知识,感谢您的分析。