一个奇特的旋转问题
问题描述
最近我遇到了一个奇怪的 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 #14:0x000000010710a74c Glip`+[UIView(instrumentation) ADEumAnimateWithDuration:delay:options:animations:completion:] +300
frame #15:0x00000...
最终,通过断点定位,我发现问题出在
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;
现在,当遇到旋转问题时,又多了一个思路可以考虑…