blog

一个奇特的旋转问题

2019-07-30
iOS 技术

问题描述

最近我遇到了一个奇怪的 bug:在连续加入和退出会议三次后,界面无法旋转。

假设

假设 1

如果界面无法旋转,并且确认旋转锁定已经关闭,首先要检查与支持方向相关的函数返回值是否正确。

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask

open override var shouldAutorotate: Bool

open override var supportedInterfaceOrientations: UIInterfaceOrientationMask

于是,我设置了三个全局断点并依次进行调试…

果然… 没有任何问题

然而,发现了一个有趣的现象。当出现无法旋转的 bug 时,这些函数不再被调用。

假设 2

如果方向支持没问题,那么重新审视问题:在重复操作三次后出现… 这让我怀疑可能与内存泄漏有关,因为之前在重复某些操作后也遇到过类似的内存问题。

所以,我进一步调试…

果然… 还是没有问题

寻找方向

排除了之前的假设后,我一度陷入困惑。经过思考,我决定从系统接口入手寻找问题。既然涉及到旋转,最终一定与系统级别的方法有关。

于是,我在-[UIViewController shouldAutorotate]处设置了一个断点。

当设备能够旋转时,调用栈如下:

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001d039de80 UIKitCore`-[UIViewController shouldAutorotate]
    frame #1: 0x00000001d039f8b8 UIKitCore`-[UIViewController _updateLastKnownInterfaceOrientationOnPresentionStack:] + 144
    frame #2: 0x00000001d03a0e84 UIKitCore`-[UIViewController window:willAnimateRotationToInterfaceOrientation:duration:newSize:] + 92
    frame #3: 0x00000001d03a8270 UIKitCore`__95-[UIViewController(AdaptiveSizing) _window:viewWillTransitionToSize:withTransitionCoordinator:]_block_invoke.3392 + 48
    frame #4: 0x00000001d03af570 UIKitCore`-[_UIViewControllerTransitionCoordinator _applyBlocks:releaseBlocks:] + 264
    frame #5: 0x00000001d03abb64 UIKitCore`-[_UIViewControllerTransitionContext __runAlongsideAnimations] + 176
    frame #6: 0x00000001d0da7f40 UIKitCore`+[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 608
    frame #7: 0x00000001d0da8480 UIKitCore`+[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] + 108
    frame #8: 0x000000010710a74c Glip`+[UIView(instrumentation) ADEumAnimateWithDuration:delay:options:animations:completion:] + 300
    frame #9: 0x00000001d03c29e8 UIKitCore`__58-[_UIWindowRotationAnimationController animateTransition:]_block_invoke_2 + 308
    frame #10: 0x00000001d0dac560 UIKitCore`+[UIView(Internal) _performBlockDelayingTriggeringResponderEvents:] + 220
    frame #11: 0x00000001d03c2778 UIKitCore`__58-[_UIWindowRotationAnimationController animateTransition:]_block_invoke + 128
    frame #12: 0x00000001d0da7f40 UIKitCore`+[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 608
    frame #13: 0x00000001d0da8480 UIKitCore`+[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] + 108
    frame #140x000000010710a74c Glip`+[UIView(instrumentation) ADEumAnimateWithDuration:delay:options:animations:completion:] +300
    frame #150x00000...

最终,通过断点定位,我发现问题出在

frame #22:  UIKitCore `__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__+20
frame#23:  UIKitCore `___CFXRegistrationPost_block_invoke+64
frame#24:  UIKitCore `_CFXRegistrationPost+392
frame#25:  UIKitCore `___CFXNotificationPost_block_invoke+96
frame#26:  UIKitCore `-[_CFXNotificationRegistrar find :object :observer :enumerator :]+1496
frame#27:  UIKitCore `_CFXNotificationPost+696
frame#28:  Foundation `-[NSNotificationCenter postNotificationName :object :userInfo :]+68
frame#29:   UIKitCore `-[UIDevice setOrientation:animated :]+328

无论 APP 是否支持旋转,[UIDevice setOrientation:animated :]都会被调用,并且参数值是正确的。关键在于下一步。当 APP 支持旋转时,会发送 UIDeviceOrientationDidChangeNotification 通知。如果不支持旋转,则不会发送通知。

这引出了一个问题:在什么情况下系统不会发送 UIDeviceOrientationDidChangeNotification 通知?

当有疑问时,请查阅文档。

在 UIDevice.h 中,我找到了这三项:

@property(nonatomic,readonly,getter=isGeneratingDeviceOrientationNotifications) BOOL generatesDeviceOrientationNotifications __TVOS_PROHIBITED;
- (void)beginGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;      // nestable
- (void)endGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;

它们似乎是罪魁祸首。

确认问题

在调试过程中,我发现当 APP 支持旋转时,isGeneratingDeviceOrientationNotifications = true;当不支持时,isGeneratingDeviceOrientationNotifications=false。

查看 generatesDeviceOrientationNotifications 的文档:

如果此属性的值为 YES,当设备改变方向时,共享 UIDevice 对象会发布 UIDeviceOrientationDidChangeNotification 通知。如果值为 NO,则不会生成方向通知。设备方向通知只能在调用 beginGeneratingDeviceOrientationNotifications 和 endGeneratingDeviceOrientationNotifications 方法之间生成。

查看 beginGeneratingDeviceOrientationNotifications 的文档:

在尝试从接收器获取方向数据之前,必须调用此方法。此方法启用设备的加速度计硬件,并开始向接收器传递加速度事件。接收器随后使用这些事件在设备方向改变时发布 UIDeviceOrientationDidChangeNotification 通知并更新 orientation 属性。 您可以安全地嵌套调用此方法,但应始终匹配每个调用与相应的 endGeneratingDeviceOrientationNotifications 方法调用。

因此,isGeneratingDeviceOrientationNotifications 的值受这两个方法影响。

似乎已经找到原因

继续调试,我发现项目代码未调用 beginGeneratingDeviceOrientationNotifications 和 endGeneratingDeviceOrientationNotifications。

总结

当调用-[UIWindow setRootViewController :]时,系统会调用 beginGeneratingDeviceOrientationNotifications。当-[UIWindow dealloc]发生时,会调用 endGeneratingDeviceOrientationNotifications。系统中的这一逻辑看起来是合理的。 问题出在 WebRTC,当启动视频会话时,它会调用 beginGeneratingDeviceOrientationNotifications,而停止视频会话时则会调用 endGeneratingDeviceOrientationNotifications。然而,在核心库代码中存在一个缺陷:启动视频会话时未调用 beginGeneratingDeviceOrientationNotifications,导致这两个方法未成对出现,从而引发错误。

以前我没有太注意这三个方面:

@property(nonatomic,readonly,getter=isGeneratingDeviceOrientationNotifications) BOOL generatesDeviceOrientationNotifications __TVOS_PROHIBITED;
- (void)beginGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;      // nestable
- (void)endGeneratingDeviceOrientationNotifications __TVOS_PROHIBITED;

现在,当遇到旋转问题时,又多了一个思路可以考虑…