前言
自 iOS 9 以后,Apple 加入了悬浮窗调试工具,也就是 UIDebuggingInformationOverlay
。利用它我们可以做到很多事情,例如:查看视图层级,控制器层级,页面中的变量,测量等等。那么我们如何开启这个调试工具呢?只需要添加如下的代码:
|
|
这段代码的实际意义可以转换成以下两行代码:
|
|
调用成功后,我们就能看到悬浮窗的庐山真面目了。
然而,在 iOS 11 后,上面的代码就不再 work 了。获取信息后得知是 Apple 添加了验证机制,想以此来确保只有内部的 App 链接到 UIKit
后才可以访问这些私有类。在 iOS 11之后,我们还有办法去使用这种调试工具吗?
内部实现探究
利用 LLDB 尝试去逆向 -[UIDebuggingInformationOverlay init]
,发现在 iOS 10 下,该方法的实现大致如下:
|
|
使用同样的方法去逆向 iOS 11 下的 -[UIDebuggingInformationOverlay init]
,该方法的实现大致是这样的:
|
|
从如上代码中我们可以看出,Apple 使用 UIDebuggingOverlayIsEnabled()
去验证当前设备是否是内部设备,所以,当我们去调用 [UIDebuggingInformationOverlay new]
时,会返回 nil
。
如何绕过验证机制
Serek Selander 在 Swizzling in iOS 11 with UIDebuggingInformationOverlay 文章中给出了他的方案。他的原理是通过 LLDB 找出 dispatch_once
部分代码在内存地址中的范围。dispatch_once
内部实现上是这样一个流程用 onceToken
的地址与 -1 进行比较,如果包含 -1 ,就表示已经执行过 block 中的代码,不再执行,若不包含 -1 , 则会去执行 block 中的代码,并将 onceToken
的地址置为 -1(默认初始化为 0)。他的做法就是找到 mainHandler.onceToken
的内存地址,然后将 -1 写入到该内存地址中。完整的代码如下:
|
|
改进
由于模拟器和真机的架构不同, Serek Selander 给出的代码只能在模拟器下 work ,因而为了能在真机下 work , 我做出了一些改进。完整代码如下:
|
|
主要思想是 UIDebuggingInformationOverlay
是 UIWindow
的子类,那么我们可以利用 runtime 机制动态去替换其 init
方法的 IMP
,以此来绕过 Apple 的验证机制。在实例化一个 UIDebuggingInformationOverlay
对象后,调用 [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture:(UIGestureRecognizer *)]
触发 UIDebuggingInformationOverlay
显示到屏幕上。由于该方法要求必须要有 UIGestureRecognizer
手势,以便检查其 state
值,因而我们需要添加 state
变量用来伪装手势。最后,在实际调用的地方调用如下代码即可 work :
|
|