为啥在 iOS 中 performSelector:withObject:@YES 时总是得到 NO,这在 macOS 中有所不同?

Posted

技术标签:

【中文标题】为啥在 iOS 中 performSelector:withObject:@YES 时总是得到 NO,这在 macOS 中有所不同?【英文标题】:Why I always get NO when performSelector:withObject:@YES in iOS, which is different in macOS?为什么在 iOS 中 performSelector:withObject:@YES 时总是得到 NO,这在 macOS 中有所不同? 【发布时间】:2018-10-05 19:02:12 【问题描述】:

我有一些 iOS 代码如下:

//ios
- (void)viewDidLoad 
    [super viewDidLoad];
    [self performSelector:@selector(handler:) withObject:@YES];


- (void)handler:(BOOL)arg   //always NO
    if(arg)   
         NSLog(@"Uh-hah!");   //won't log
    

我知道我不应该这样写。这是错误的,因为@YES 是一个对象,我应该接收一个id 作为参数并在handler: 中将其拆箱,例如:

- (void)handler:(id)arg 
    if([arg boolValue]) ...

作为一个错误的代码,对于任何类的任何其他对象而不是@YES,我总是得到arg == NO。问题是,为什么ON EARTH bool arg 总是NO? 我做了一些研究,以下是我所学到的:

在 iOS 中,BOOL 实际上是 C 中的 _Bool(或宏 bool)(_Bool keyword)

在 macOS 中,BOOL 实际上是signed char

如果我创建一个相同的 ma​​cOS 代码,我会得到不同的结果,例如:

//macOS
- (void)viewDidLoad 
    [super viewDidLoad];
    [self performSelector:@selector(handler:) withObject:@YES];  //@YES's address: say 0x00007fffa38533e8


- (void)handler:(BOOL)arg   //\xe8 (=-24)
    if(arg) 
         NSLog(@"Uh-hah!");  //"Uh-huh!"
    

这是有道理的,因为BOOL 只是有符号字符,参数是从@YES 对象地址的最低字节转换而来的。 但是,此解释不适用于 iOS 代码。我认为任何非零数都会被强制转换为真(并且地址本身必须非零)。但是为什么我没有? *

【问题讨论】:

【参考方案1】:

-[NSObject performSelector:withObject:] 只应该与只接受一个对象指针参数并返回一个对象指针的方法一起使用。您的方法采用BOOL 参数,而不是对象指针参数,因此它不能与-[NSObject performSelector:withObject:] 一起使用。

如果你总是要发送消息handler:,并且你知道该方法有一个 BOOL 参数,你应该直接调用它:

[self handler:YES];

如果方法的名称将在运行时动态确定,但您知道方法的签名将始终相同(在这种情况下,只有一个 BOOL 类型的参数,不返回任何内容),您可以使用 @ 调用它987654327@。在调用该方法的底层实现函数之前,您必须将objc_msgSend 转换为适当的函数类型(请记住,Objective-C 方法的实现函数的前两个参数是self_cmd,然后是声明的参数)。这是因为objc_msgSend 是一个蹦床,它调用适当的实现函数,所有用于存储参数的寄存器和堆栈都完好无损,因此您必须使用实现函数的调用约定来调用它。在你的情况下,你会这样做:

SEL selector = @selector(handler:); // assume this is computed dynamically at runtime
((void (*)(id, SEL, BOOL))objc_msgSend)(self, selector, YES);

顺便说一下,如果您查看-[NSObject performSelector:withObject:] 的source code,您会发现它们做同样的事情——它们知道方法的签名必须是id 类型的一个参数和一个返回类型为id,因此他们将objc_msgSend 转换为id (*)(id, SEL, id),然后调用它。

在极少数情况下,方法的签名也会动态变化并且在编译时未知,那么您将不得不使用NSInvocation

让我们考虑一下当您将-[NSObject performSelector:withObject:] 与错误签名的方法一起使用时发生的情况。在里面,他们调用objc_msgSend(),相当于调用底层实现函数,函数指针类型为id (*)(id, SEL, id)。但是,您方法的实现函数实际上具有void (id, SEL, BOOL) 类型。因此,您正在使用不同类型的函数指针调用函数。根据 C 标准(C99 标准,第 6.5.2.2 节,第 9 段):

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

所以基本上你看到的是未定义的行为。未定义的行为意味着任何事情都可能发生。如您所见,它可能会在一个系统上返回一件事,而在另一个系统上返回另一件事,或者它可能会使整个程序崩溃。你不能依赖任何特定的行为。

【讨论】:

哇!非常感谢您提供如此完美、详细和令人信服的答案!【参考方案2】:

问题在于处理程序的声明。在这种情况下,处理程序的参数类型应该是 id (Objective C) 或 Any (Swift)。

- (void)handler:(id)arg 
    if(arg)    // Would be same as object passed
         NSLog(@"Uh-hah!");
    

【讨论】:

不,兄弟。 - (id)performSelector:(SEL)aSelector withObject:(id)object; 要求一个 id 类型作为参数,你的代码甚至不会编译。 我同意将 Bool 作为@YES 传递。但是将 args 的参数类型更改为 id。它应该可以工作。【参考方案3】:

答案只是有问题..

在 iOS 中,BOOL 声明为:

'typedef bool BOOL' 你也提到过这个..

所以对于 iOS,它将默认值为 false。所以你的日志永远不会被打印出来。

【讨论】:

为什么将默认值设为false? bool 是否有“默认”值?我通过的论点做了什么(@YES 或任何东西)?请您详细解释一下好吗? 嘿... false 始终是 bool 的默认值,因此在您的情况下它为 false。

以上是关于为啥在 iOS 中 performSelector:withObject:@YES 时总是得到 NO,这在 macOS 中有所不同?的主要内容,如果未能解决你的问题,请参考以下文章

ios performselector:withobject:withobject:怎么解决

performSelector "backgroundRefreshStatus" 在 iOS 7 上崩溃

iOS 消息处理之performSelector

iOS performSelector方法总结

ios performSelector:withObject:afterDelay: in viewDidLoad 冻结视图推送

iOS - 如何实现具有多个参数和 afterDelay 的 performSelector?