A Peculiar Rotation Issue

July 30, 2019 (5y ago)

Phenomenon

I recently encountered a bizarre bug: after joining and exiting a meeting three times, the interface becomes unable to rotate.

Hypotheses

Hypothesis 1

If rotation is not possible, and the rotation lock is confirmed to be off, the first thought is to check if the return values of the support orientation related functions are correct.

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

open override var shouldAutorotate: Bool 

open override var supportedInterfaceOrientations: UIInterfaceOrientationMask

So, I set three global breakpoints and debugged them one by one...

Sure enough... There were no issues...

However, an interesting phenomenon was observed. When the bug (inability to rotate) occurred, these functions were no longer called.

Hypothesis 2

If the support orientation isn't the issue, revisiting the phenomenon: it happens after repeating the action three times... This led me to speculate that it might be related to a memory leak, as I had previously encountered similar symptoms with memory-related issues after repeating certain actions.

So, I debugged further...

And sure enough... Still no issues...

Finding the Direction

With the previous hypotheses ruled out, I was initially perplexed and clueless. After some thought, I decided to look for issues starting from the system interface. Since rotation is involved, it must ultimately boil down to a method at the system level.

So, I set a breakpoint at -[UIViewController shouldAutorotate]

When the device is able to rotate, the call stack is as follows:

* 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: 0x00000001d03c2650 UIKitCore`-[_UIWindowRotationAnimationController animateTransition:] + 456
    frame #16: 0x00000001d0968398 UIKitCore`-[UIWindow _rotateToBounds:withAnimator:transitionContext:] + 580
    frame #17: 0x00000001d096aafc UIKitCore`-[UIWindow _rotateWindowToOrientation:updateStatusBar:duration:skipCallbacks:] + 1184
    frame #18: 0x00000001d096b1b0 UIKitCore`-[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 516
    frame #19: 0x00000001d096a5b0 UIKitCore`-[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 128
    frame #20: 0x00000001d096925c UIKitCore`__57-[UIWindow _updateToInterfaceOrientation:duration:force:]_block_invoke + 124
    frame #21: 0x00000001d0969160 UIKitCore`-[UIWindow _updateToInterfaceOrientation:duration:force:] + 560
    frame #22: 0x00000001a43595bc CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
    frame #23: 0x00000001a4359588 CoreFoundation`___CFXRegistrationPost_block_invoke + 64
    frame #24: 0x00000001a4358a7c CoreFoundation`_CFXRegistrationPost + 392
    frame #25: 0x00000001a4358728 CoreFoundation`___CFXNotificationPost_block_invoke + 96
    frame #26: 0x00000001a42d2524 CoreFoundation`-[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1496
    frame #27: 0x00000001a43581d8 CoreFoundation`_CFXNotificationPost + 696
    frame #28: 0x00000001a4d40814 Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 68
    frame #29: 0x00000001d05c2030 UIKitCore`-[UIDevice setOrientation:animated:] + 328
    frame #30: 0x00000001d01f071c UIKitCore`__124-[_UICanvasDeviceOrientationSettingsDiffAction _updateDeviceOrientationWithSettingObserverContext:canvas:transitionContext:]_block_invoke + 88
    frame #31: 0x00000001d01f2304 UIKitCore`_performChangesWithTransitionContext + 836
    frame #32: 0x00000001d01f0688 UIKitCore`-[_UICanvasDeviceOrientationSettingsDiffAction _updateDeviceOrientationWithSettingObserverContext:canvas:transitionContext:] + 236
    frame #33: 0x00000001d01f058c UIKitCore`__133-[_UICanvasDeviceOrientationSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 104
    frame #34: 0x00000001d01f1fb0 UIKitCore`_performActionsWithDelayForTransitionContext + 112
    frame #35: 0x00000001d01f04e0 UIKitCore`-[_UICanvasDeviceOrientationSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 172
    frame #36: 0x00000001d01f5d84 UIKitCore`-[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 360
    frame #37: 0x00000001d05253b8 UIKitCore`-[UIApplicationSceneClientAgent scene:handleEvent:withCompletion:] + 464
    frame #38: 0x00000001a6d63920 FrontBoardServices`__80-[FBSSceneImpl updater:didUpdateSettings:withDiff:transitionContext:completion:]_block_invoke_3 + 224
    frame #39: 0x00000001356e4c74 libdispatch.dylib`_dispatch_client_callout + 16
    frame #40: 0x00000001356e8840 libdispatch.dylib`_dispatch_block_invoke_direct + 232
    frame #41: 0x00000001a6d9d0bc FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 40
    frame #42: 0x00000001a6d9cd58 FrontBoardServices`-[FBSSerialQueue _performNext] + 408
    frame #43: 0x00000001a6d9d310 FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 52
    frame #44: 0x00000001a437a2bc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #45: 0x00000001a437a23c CoreFoundation`__CFRunLoopDoSource0 + 88
    frame #46: 0x00000001a4379b24 CoreFoundation`__CFRunLoopDoSources0 + 176
    frame #47: 0x00000001a4374a60 CoreFoundation`__CFRunLoopRun + 1004
    frame #48: 0x00000001a4374354 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #49: 0x00000001a657479c GraphicsServices`GSEventRunModal + 104
    frame #50: 0x00000001d092bb68 UIKitCore`UIApplicationMain + 212
    frame #51: 0x000000010525d370 Glip`main at AppDelegate.swift:67:7
    frame #52: 0x00000001a3e3a8e0 libdyld.dylib`start + 4

Ultimately, through breakpoint positioning, I discovered that the issue lies in

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

Regardless of whether the APP supports rotation or not, [UIDevice setOrientation:animated:] is always called, and the parameter values are correct. The key lies in the next step. When the APP supports rotation, a UIDeviceOrientationDidChangeNotification notification is sent out. If it doesn't support rotation, the notification is not sent.

This led me to the question: Under what circumstances might the system not send out a UIDeviceOrientationDidChangeNotification notification?

When in doubt, consult the documentation.

In UIDevice.h, I found these three items:

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

They seemed to be the culprits.

Identifying the Issue

During debugging, I found that when the APP supports rotation, isGeneratingDeviceOrientationNotifications = true; when it doesn't, isGeneratingDeviceOrientationNotifications=false.

Reviewing the documentation for generatesDeviceOrientationNotifications:

If the value of this property is YES, the shared UIDevice object posts a UIDeviceOrientationDidChangeNotification notification when the device changes orientation. If the value is NO, it generates no orientation notifications. Device orientation notifications can only be generated between calls to the beginGeneratingDeviceOrientationNotifications and endGeneratingDeviceOrientationNotifications methods.

Reviewing the documentation for beginGeneratingDeviceOrientationNotifications:

You must call this method before attempting to get orientation data from the receiver. This method enables the device’s accelerometer hardware and begins the delivery of acceleration events to the receiver. The receiver subsequently uses these events to post UIDeviceOrientationDidChangeNotification notifications when the device orientation changes and to update the orientation property. You may nest calls to this method safely, but you should always match each call with a corresponding call to the endGeneratingDeviceOrientationNotifications method.

Thus, the value of isGeneratingDeviceOrientationNotifications is influenced by these two methods.

It seems the cause has been identified

Continuing with debugging, I found that the project's code does not call beginGeneratingDeviceOrientationNotifications and endGeneratingDeviceOrientationNotifications.

Conclusion

When -[UIWindow setRootViewController:] is called, the system invokes beginGeneratingDeviceOrientationNotifications. When -[UIWindow dealloc] occurs, it calls endGeneratingDeviceOrientationNotifications. This logic in the system appears to be sound. The issue stems from WebRTC, which calls beginGeneratingDeviceOrientationNotifications when starting a video session and endGeneratingDeviceOrientationNotifications when stopping it. However, there's a flaw in the CoreLib code: beginGeneratingDeviceOrientationNotifications is not called when starting a video session, causing these two to not appear in pairs, thus leading to the error.

I hadn't paid much attention to these three aspects before:

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

Now, when facing rotation issues, I have one more approach to consider...