-
-
Save steipete/8df39fea0d39680a7a6b to your computer and use it in GitHub Desktop.
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 |
Came up with a workaround, but it's not pretty. https://gist.github.com/steipete/d928debb92e86de89eb2
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.
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.
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.
@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.
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.
So when the rotation event is dispatched,
[UIDevice setOrientation:animated:]
gets called. It checks if there are any device orientation observers (which is the case if there are any UIWindows), then does some other checks including some special code for springboard, before sending theUIDeviceOrientationDidChangeNotification
.Every window is a listener to this notification. it evaluates the UIDeviceOrientationRotateAnimatedUserInfoKey payload and calls
[UIWindow updateInterfaceOrientationFromDeviceOrientationIfRotationEnabled:]
.updateInterfaceOrientationFromDeviceOrientationIfRotationEnabled
does what we'd expect from the name, it's a small shim that checksisInterfaceAutorotationDisabled
and then calls[UIWindow _updateInterfaceOrientationFromDeviceOrientation:]
.Looking at this method we see that quite a bit on the internals changed; as rotating a window on iOS 8 now changes the frame and no longer just applies a transform as it did on 7. Interesting tidbit: Apple uses a
UIIntegralTransform()
function to round it's CGAffineTransform values.When you set a
rootViewController
to an UIWindow, it becomes it's delegate as well (delegate isn't public though, but this happens implicit).The difference between 7 and 8 for the regression I am hunting is that on 7 things stop in
-[UIWindow _updateToInterfaceOrientation:duration:force:]
, while it continues the path to-[UIWindow _setRotatableViewOrientation:duration:force:]
on 8.To make things bit more complicated, the interesting logic is actually inside a block called in there (
__57__UIWindow__updateToInterfaceOrientation_duration_force___block_invoke
)When breaking in
- (BOOL)_shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
we notice that this is called once on iOS 7 where it returns NO (as expected); but twice on iOS 8 where it returns YES for our hidden window (as the delegate/rootViewController in there doesn't limit rotation)That was actually the hint I needed - looks like this whole process is called only on they kewWindow for 7 but on all windows for 8. In fact, not even
_handleDeviceOrientationChange
is called on our hidden window in 7.Let's investigate where this is registered on NSNotification. On iOS 7 this happens in
-[UIWindow setAutorotates:forceUpdateInterfaceOrientation:]
(it's also referenced inendDisablingInterfaceAutorotationAnimated
but that's not relevant for now)So let's step in where this is being called!
iOS 7:
iOS 8:
AND
So while 7 seems to trigger autorotation when we call
makeKeyAndVisible
, iOS 8 does this earlier as soon as a rootViewController is set.setDelegate:
is called in both paths and decides if it should configure autorotation.Looking at
-[UIWindow setDelegate:]
, things became quite a bit more complex in 8 as well. However, this calls- (void)setAutorotates:(BOOL)autorotates forceUpdateInterfaceOrientation:(BOOL)force
in all cases... so this can't be it either.Playing with my example project reveal something different; delegate is actually not always set when we set the rootViewController - only sometimes! On iOS 7 it's nil unless we become the keyWindow... on 8 it's being set inside
_tryBecomeRootViewControllerInWindow
which is called insetRootViewController:
Looking at
-[UIWindow setRootViewController:]
there's a new UIApplicationLinkedOnOrAfter check for iOS 8 that... guess what... callstryBecomeRootViewControllerInWindow:
if running iOS 8.So now we know why this is happening and at least this part seems intentional - however the rotation consequences seem less so. But it was fun to dig deep into rotation handling and it gave me a much better idea about how the system works.