由于使用了 Swift 中不推荐的调度方法,您遇到了一个极端情况。
请注意,您在下面阅读的所有内容都与 Objective-C 消息传递(也称为方法调用)有关。在与 Objective-C 类互操作时,Swift 编译器生成(或多或少)与 Objective-C 编译器相同的调用者代码,因此适用于 Objc 的任何内容也适用于调用 ObjC 的 Swift 代码。
首先,performSelector 不应该用在返回浮点数的方法上,因为在幕后performSelector 调用objc_msgSend,但是对于返回浮点数的方法,objc_msgSend 的内部结果是从位置不对。
objc_msgSend 的一点背景 - 这个函数是 Objective-C 动态调度的核心,基本上任何对 Objective-C 对象的方法调用都会导致objc_msgSend 被调用。这里就不赘述了,网上有很多关于objc_msgSend的资料,只是想总结一下,这个函数是一个非常通用的函数,可以强制转换成任何方法签名。
那么,让我们以brightness 为例。使用performSelector(Swift perform(_:) 中命名的 Objective-C 方法)时,幕后究竟发生了什么? performSelector 的实现大致是这样的:
- (void)performSelector:(SEL)selector) {
return objc_msgSend(self, selector);
}
基本上该方法只是转发调用objc_msgSend产生的值。
现在事情变得有趣了。由于浮点数和整数使用不同的返回位置,编译器需要根据其对objc_msgSend 返回的数据类型的了解来生成不同的读取位置。
如果编译器认为objcSend 将返回的内容与函数实际返回的内容之间存在误解,那么可能会发生不好的事情。但在你的情况下,有一个“幸运的”巧合,因为你的程序没有崩溃。
基本上,您的perform(Selector("brightness") 调用会导致objc_msgSend(mainScreen, Selector("brightness")) 调用假定objc_msgSend 将返回一个对象。但反过来被调用的代码 - brightness getter - 返回一个浮点数。
那么为什么当 Swift 代码尝试打印结果时应用程序没有崩溃(实际上它应该在 takeUnretainedValue 上崩溃)?
这是因为浮点数和整数(以及对象指针与 int 属于同一类别)具有不同的返回位置。 performSelector 从返回位置读取整数,因为它需要一个对象指针。幸运的是,在那个时间点,最后调用的方法很可能是返回 UIDevice 实例的方法。我假设在内部 UIScreen.brightness 向设备对象询问此信息。
会发生这样的事情:
1. Swift code calls mainScreen?.perform(Selector("brightness")
2. This results in Objective-C call [mainScreen performSelector:selector]
3. objc_msgSend(mainScreen, selector) is called
3. The implementation of UIScreen.brightness is executed
4. UIScreen.brightness calls UIDevice.current (or some other factory method)
5. UIDevice.current stores the `UIDevice` instance in the int return location
6. UIScreen.brightness calls `device.someMemberThatReturnsTheBrightness`
7. device.someMemberThatReturnsTheBrightness stores the brightness into
the float return location
8. UIScreen.brightness exits, its results is already at the proper location
9. performSelector exits
10. The Swift code expecting an object pointer due to the signature of
performSelector reads the value from the int location, which holds the
value stored at step #5
基本上,这一切都与调用约定有关,以及您的“无辜”代码如何被翻译成汇编指令。这就是不推荐使用完全动态分派的原因,因为编译器不具备构建可靠汇编指令集的所有细节。