UIScreen 亮度上的 Swift 选择器在调用时返回 UIDevice

Posted

技术标签:

【中文标题】UIScreen 亮度上的 Swift 选择器在调用时返回 UIDevice【英文标题】:Swift Selector on UIScreen brightness returning UIDevice when invoked 【发布时间】:2021-02-24 05:27:15 【问题描述】:

我有以下sn-p

    let classByName = objc_lookUpClass("UIScreen")


    let mainScreen = (classByName as? NSObjectProtocol)?.perform(Selector("mainScreen"))?.takeRetainedValue()
    print(mainScreen) // Optional(<UIScreen: ....
    print(mainScreen?.perform(Selector("brightness")).takeUnretainedValue()) // Optional(<UIDevice:...

如您所见,第二种方法返回的是对当前 UIDevice 的引用,而不是对应于屏幕亮度的 CGFloat...

知道这里发生了什么吗?

【问题讨论】:

为什么不直接使用UIScreen.main.brightness 因为这不好玩:)。 Jk,我正在尝试更多地了解选择器的工作原理。 【参考方案1】:

由于使用了 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

基本上,这一切都与调用约定有关,以及您的“无辜”代码如何被翻译成汇编指令。这就是不推荐使用完全动态分派的原因,因为编译器不具备构建可靠汇编指令集的所有细节。

【讨论】:

以上是关于UIScreen 亮度上的 Swift 选择器在调用时返回 UIDevice的主要内容,如果未能解决你的问题,请参考以下文章

UIScreen 亮度属性

swift 选择器在swift中

如何在 iOS 5 应用程序中更改亮度?

Xcode / Swift / UIScreen.main.bounds.size.height 返回 0

记一个swift UIscreen界面问题

UIScreen.mainScreen().nativeBounds.height 无法使用 Xcode 7/Swift 2,目标 iOS7