Skip to content

Instantly share code, notes, and snippets.

@steipete
Last active September 17, 2020 23:24
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save steipete/8df39fea0d39680a7a6b to your computer and use it in GitHub Desktop.
Save steipete/8df39fea0d39680a7a6b to your computer and use it in GitHub Desktop.
Hunting down a regression in interface rotation on iOS 8 with multiple windows. (rdar://19592583)
This is the code path that changed the status bar orientation on iOS 7:
* thread #1: tid = 0x698dbf, 0x00085830 WindowRotationIssue`-[AppDelegate application:willChangeStatusBarOrientation:duration:](self=0x7a041f90, _cmd=0x0137a18d, application=0x79e39cc0, newStatusBarOrientation=UIInterfaceOrientationPortraitUpsideDown, duration=0.80000000000000004) + 96 at AppDelegate.m:23, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x00085830 WindowRotationIssue`-[AppDelegate application:willChangeStatusBarOrientation:duration:](self=0x7a041f90, _cmd=0x0137a18d, application=0x79e39cc0, newStatusBarOrientation=UIInterfaceOrientationPortraitUpsideDown, duration=0.80000000000000004) + 96 at AppDelegate.m:23
frame #1: 0x00bb6ab5 UIKit`-[UIApplication setStatusBarOrientation:animationParameters:notifySpringBoardAndFence:] + 242
frame #2: 0x00bfa8e4 UIKit`-[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 4761
frame #3: 0x00bf9646 UIKit`-[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:] + 82
frame #4: 0x00bf9518 UIKit`-[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 117
frame #5: 0x00bf95a0 UIKit`-[UIWindow _setRotatableViewOrientation:duration:force:] + 67
frame #6: 0x00bf863a UIKit`__57-[UIWindow _updateToInterfaceOrientation:duration:force:]_block_invoke + 120
frame #7: 0x00bf859c UIKit`-[UIWindow _updateToInterfaceOrientation:duration:force:] + 400
frame #8: 0x00bf8cd6 UIKit`-[UIWindow _updateInterfaceOrientationFromDeviceOrientation:] + 1346
frame #9: 0x00bf878d UIKit`-[UIWindow _updateInterfaceOrientationFromDeviceOrientationIfRotationEnabled:] + 94
frame #10: 0x00bf8399 UIKit`-[UIWindow _handleDeviceOrientationChange:] + 122
frame #11: 0x00188049 Foundation`__57-[NSNotificationCenter addObserver:selector:name:object:]_block_invoke + 40
frame #12: 0x00797f04 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
frame #13: 0x006efefb CoreFoundation`_CFXNotificationPost + 2859
frame #14: 0x000c1e41 Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 98
frame #15: 0x00e0b5cf UIKit`-[UIDevice setOrientation:animated:] + 295
frame #16: 0x00bc64fb UIKit`-[UIApplication handleEvent:withNewEvent:] + 806
frame #17: 0x00bc7555 UIKit`-[UIApplication sendEvent:] + 85
frame #18: 0x00bb4250 UIKit`_UIApplicationHandleEvent + 683
Things got quite a bit more complex in iOS 8:
(Notice the new _UIWindowRotationAnimationController)
* thread #1: tid = 0x69b266, 0x0009e830 WindowRotationIssue`-[AppDelegate application:willChangeStatusBarOrientation:duration:](self=0x7af199e0, _cmd=0x017c1468, application=0x7b312ea0, newStatusBarOrientation=UIInterfaceOrientationLandscapeRight, duration=0) + 96 at AppDelegate.m:23, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x0009e830 WindowRotationIssue`-[AppDelegate application:willChangeStatusBarOrientation:duration:](self=0x7af199e0, _cmd=0x017c1468, application=0x7b312ea0, newStatusBarOrientation=UIInterfaceOrientationLandscapeRight, duration=0) + 96 at AppDelegate.m:23
frame #1: 0x00ebed3f UIKit`-[UIApplication setStatusBarOrientation:animationParameters:notifySpringBoardAndFence:] + 248
frame #2: 0x00f13cc6 UIKit`__78-[UIWindow _rotateWindowToOrientation:updateStatusBar:duration:skipCallbacks:]_block_invoke + 329
frame #3: 0x013f7062 UIKit`__58-[_UIWindowRotationAnimationController animateTransition:]_block_invoke + 45
frame #4: 0x013f7019 UIKit`-[_UIWindowRotationAnimationController animateTransition:] + 475
frame #5: 0x00f1110f UIKit`-[UIWindow _rotateToBounds:withAnimator:transitionContext:] + 877
frame #6: 0x00f13a21 UIKit`-[UIWindow _rotateWindowToOrientation:updateStatusBar:duration:skipCallbacks:] + 2143
frame #7: 0x00f158a8 UIKit`-[UIWindow _setRotatableClient:toOrientation:applyTransformToWindow:updateStatusBar:duration:force:isRotating:] + 6839
frame #8: 0x00f13120 UIKit`-[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:isRotating:] + 128
frame #9: 0x00f13099 UIKit`-[UIWindow _setRotatableClient:toOrientation:updateStatusBar:duration:force:] + 84
frame #10: 0x00f12e7f UIKit`-[UIWindow _setRotatableViewOrientation:updateStatusBar:duration:force:] + 138
frame #11: 0x00f12fec UIKit`-[UIWindow _setRotatableViewOrientation:duration:force:] + 68
frame #12: 0x00f11cba UIKit`__57-[UIWindow _updateToInterfaceOrientation:duration:force:]_block_invoke + 130
frame #13: 0x00f11c17 UIKit`-[UIWindow _updateToInterfaceOrientation:duration:force:] + 425
frame #14: 0x00f124d8 UIKit`-[UIWindow _updateInterfaceOrientationFromDeviceOrientation:] + 1397
frame #15: 0x00f11f5c UIKit`-[UIWindow _updateInterfaceOrientationFromDeviceOrientationIfRotationEnabled:] + 93
frame #16: 0x00f119fb UIKit`-[UIWindow _handleDeviceOrientationChange:] + 122
frame #17: 0x0012bc49 Foundation`__57-[NSNotificationCenter addObserver:selector:name:object:]_block_invoke + 40
frame #18: 0x008bf4a4 CoreFoundation`__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 20
frame #19: 0x007ad03b CoreFoundation`_CFXNotificationPost + 3051
frame #20: 0x0011b246 Foundation`-[NSNotificationCenter postNotificationName:object:userInfo:] + 98
frame #21: 0x01187040 UIKit`-[UIDevice setOrientation:animated:] + 307
frame #22: 0x00ed2f09 UIKit`-[UIApplication handleEvent:withNewEvent:] + 1364
frame #23: 0x00ed34ac UIKit`-[UIApplication sendEvent:] + 85
frame #24: 0x00ebbbe3 UIKit`_UIApplicationHandleEvent + 704
@steipete
Copy link
Author

@steipete
Copy link
Author

Came up with a workaround, but it's not pretty. https://gist.github.com/steipete/d928debb92e86de89eb2

@carllindberg
Copy link

I thought that in iOS8, the root view controller (via the -shouldAutorotate method) controls whether its window rotates. In iOS7 or before, windows never rotated -- the rotation transform was applied to the root view controller's view. Would it not be possible to implement that method to return NO if self.view.window.hidden is YES ? I have had the opposite problem -- a window was not rotating when we wanted it to, which happened because there was a period of time where the window had no root view controller and that was when rotation happened. Setting a placeholder root view controller allowed it to rotate.

@carllindberg
Copy link

Also, the root view controller should have its view loaded. I don't think setRootViewController: automatically does that anymore. I saw behavioral differences between when I set a loaded vs not-yet-loaded view controller as the root.

@grzegorzduda
Copy link

Setting rootViewController = nil (or any other value), will not remove any of the modal view controllers presented by the root view controller (or its descendants) from the view hierarchy. Those view controllers neither will be deallocated.

I guess there is some memory leak, most probably in the UIKit, perhaps this could be a clue:
http://stackoverflow.com/questions/26763020/leaking-views-when-changing-rootviewcontroller-inside-transitionwithview

What happens here is that presenting view controller (e.g. root view controller) is removed from the view hierarchy, but it is not deallocated as presented modal view controller holds strong reference to it.

If root view controller is stored in a property (e.g. realViewController), and we set rootViewController = nil, then rootViewController = realViewController (window.hidden = NO), previously used presenting view controller will not be added back to the view hierarchy and presented view controller will remain untouched (may be seen as leaking modal view controller). This side effect will be visible only for view controllers which do not occupy the whole screen (e.g. popovers, view controllers presented with UIModalPresentationFormSheet, etc.). User would see view controller and black screen behind it. Seems that the only way to deallocate modal view controller, is to dismiss it.

It looks like there is another UIKit bug which requires workaround, and workaround proposed by @steipete should be used very carefully. Please correct me if I am wrong.

@jpohhh
Copy link

jpohhh commented Feb 27, 2015

@grzegorzduda you're correct, I've been seeing this since 8.0. Specifically, a UISplitViewController with a presented view controller leaking both the SplitViewController and the presented view controller, until the next time another UISplitViewController (not just any UIViewController!) becomes the window's root view controller.

@carllindberg
Copy link

I have not gotten windows to deallocate easily in iOS8 at all if they have presented view controllers. I think that is a separate issue than this gist though. It seems any presented view controller has an inherent retain loop with the window, and dismissing the view controllers seems to be the only way to get them removed.

There is a separate problem with UISplitViewController, in that it installs a private _shouldPreventRotationHook block in the UIWindow, which has a strong retain to the UISplitViewController, and does not clean it up (nor does UIWindow release it when the window gets dealloced). That sounds like it might be the same issue (it would take another UISplitViewController to overwrite the original block). I think that may have been fixed in iOS 8.3, at least the UIWindow dealloc portion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment