ui亮度的快速选择器,当调用时返回UIDevice



我有以下代码片段

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对应于屏幕亮度…

你知道这是怎么回事吗?

由于使用Swift中不推荐的分派方法,你遇到了一个极端情况。

请注意,你在下面读到的所有内容都与Objective-C的消息传递(又名方法调用)有关。当与Objective-C类互操作时,Swift编译器生成(或多或少)与Objective-C编译器相同的调用者代码,因此任何适用于Objc的内容,也适用于调用Objc的Swift代码。


首先,performSelector不应该用于返回float的方法,因为在幕后performSelector调用objc_msgSend,但是对于返回float的方法,objc_msgSend的内部结果是从错误的位置读取的。

关于objc_msgSend的一点背景知识——这个函数是Objective-C动态调度的核心,基本上任何对Objective-C对象的方法调用都会导致objc_msgSend被调用。我不会在这里讲太多的细节,网上有很多关于objc_msgSend的资料,只是想总结一下,这个函数是一个非常通用的函数,可以被强制转换以匹配任何方法签名。

那么,让我们以brightness为例。当使用performSelector(在Swiftperform(_:)中命名的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将返回一个对象。但是反过来,被调用的代码——brightnessgetter——返回一个浮点数。

那么为什么当Swift代码试图打印结果时应用程序不会崩溃(实际上它应该在takeUnretainedValue上崩溃)?

这是因为float和int(和对象指针属于int的同一类别)有不同的返回位置。performSelector从返回位置读取int类型,因为它需要一个对象指针。幸运的是,在那个时间点上,最后调用的方法可能是返回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

基本上,这都是关于调用约定,以及你如何"无辜"。代码被翻译成汇编指令。这就是不推荐完全动态分派的原因,因为编译器没有构造一组可靠的汇编指令的所有细节。

最新更新