App支持横竖屏逻辑
因为App本身在手机场景下,是不考虑支持横屏的,但是在部分页面(场景下),需要支持强制(自动)旋转横竖屏。传统的通过设置每一个 VC 的supportedInterfaceOrientations
来控制是否旋转,灵活性不足,所以目前采取的方案是通过 AppDelegate 的supportedInterfaceOrientationsForWindow
方法,来动态调整App支持的所有方向,具体代码如下:
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
switch (self.orientationType) {
case QINOrientationTypeAuto:
return UIInterfaceOrientationMaskAll;
case QINOrientationTypeLand:
return UIInterfaceOrientationMaskLandscape;
case QINOrientationTypePor:
return UIInterfaceOrientationMaskPortrait;
}
}
手动横屏
当App需要手动切换到横屏模式时,通过修改AppDeleate的orientationType值为QINOrientationTypeLand, 来达到支持横屏的状态,具体代码如下:
+ (void)rotateByIncovationWithOrientaion:(UIInterfaceOrientation)orientation {
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
BOOL isLandscape = (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight);
delegate.orientationType = isLandscape ? QINOrientationTypeLand : QINOrientationTypePor;
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = (int)orientation;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
这里需要注意,代码是先执行了delegate.orientationType = isLandscape ? QINOrientationTypeLand : QINOrientationTypePor;
让App支持横屏, 再通过Invocation直接强制修改Orientation的朝向。
自动旋转
如果是自动旋转的场景,会通过监听事件,根据 Orientation 朝向手动修改 orientationType
, 再强制横屏/竖屏, 具体代码:
QINAddUniqueNotify(self, UIDeviceOrientationDidChangeNotification, @"handleDeviceOrientationDidChange:");
...
- (void)handleDeviceOrientationDidChange:(NSNotification *)notification {
if ([UIDevice isIPad]) {
...
} else {
if (isPortrait) {
[self changePortrait];
} else if(isLandscape) {
self.isForceLandscape = true;
...
[.. rotateByIncovationWithOrientaion:orientation];
}
}
}
当开始测试时,所有测试在 Debug 下都没有问题,但当进行 TestFlight 版本测试时,当使用自动旋转时,却发现App并没有如预期的旋转到横屏,而是停留在了竖屏状态,但是如果通过手动旋转功能时一切又正常了,于是便开始了寻找 Bug 之路。
寻找问题
简单的分析后,尝试通过 Release 运行 App,发现问题依然存在,基本可以确定,问题出在了 Release 版本上。
检查了 App 内所有 Debug 环境才生效的代码,发现并没有相关的代码存在,那么问题莫非是 Release 环境下的编译器出问题了?接着将Release版本的Optimize Level
的级别从 -Os 改为 None 进行编译,果然问题消失了,那这里基本确定应该是编译器优化的锅。
按照常识分析,编译器在将代码转换成汇编时,往往都会对执行顺序进行深度优化,考虑到寄存器和指针的移动等各种复杂因素,代码的执行顺序经常会和原始代码有所不同,这也是 Release 环境下调试时,经常发现 po 查看变量时,往往会无法读取正确的内存的原因。
再重新 Review 下 rotateByIncovationWithOrientaion
这个函数,在 invocation 实际调用前,是必须要对 property 进行修改,才能让旋转生效,莫非在这里编译器改变了执行顺序?
Debug 环境下的代码:
`+[UIViewController(QINUtils) rotateByIncovationWithOrientaion:]:
...
0x103538ca8 <+180>: adrp x8, 16063
0x103538cac <+184>: ldr x1, [x8, #0xdb0]
// bl命令执行设置 AppDelegate的orientationType属性
0x103538cb0 <+188>: bl 0x1064bc430 ; symbol stub for: objc_msgSend
0x103538cb4 <+192>: adrp x0, 15312
0x103538cb8 <+196>: add x0, x0, #0x730 ; @"setOrientation:"
0x103538cbc <+200>: bl 0x1064b6580 ; symbol stub for: NSSelectorFromString
...
0x103538da8 <+436>: adrp x8, 16072
0x103538dac <+440>: ldr x1, [x8, #0x2c0]
// bl执行 invocation 方法
0x103538db0 <+444>: bl 0x1064bc430 ; symbol stub for: objc_msgSend
0x103538db4 <+448>: ldr x0, [sp, #0x30]
0x103538db8 <+452>: mov x1, #0x0
...
// 退出方法
0x103538dd8 <+484>: ret
Release 环境下的代码基本一致:
`+[UIViewController(QINUtils) rotateByIncovationWithOrientaion:]:
...
0x103538ca8 <+180>: adrp x8, 16063
0x103538cac <+184>: ldr x1, [x8, #0xdb0]
// bl命令执行设置 AppDelegate的orientationType属性
0x103538cb0 <+188>: bl 0x1064bc430 ; symbol stub for: objc_msgSend
0x103538cb4 <+192>: adrp x0, 15312
0x103538cb8 <+196>: add x0, x0, #0x730 ; @"setOrientation:"
0x103538cbc <+200>: bl 0x1064b6580 ; symbol stub for: NSSelectorFromString
...
0x103538da8 <+436>: adrp x8, 16072
0x103538dac <+440>: ldr x1, [x8, #0x2c0]
// bl执行 invocation 方法
0x103538db0 <+444>: bl 0x1064bc430 ; symbol stub for: objc_msgSend
0x103538db4 <+448>: ldr x0, [sp, #0x30]
0x103538db8 <+452>: mov x1, #0x0
...
// 退出方法
0x103538dd8 <+484>: ret
这里就发现,似乎并不是我们想的rotateByIncovationWithOrientaion
方法执行顺序错了,那问题出在哪呢?
这条思路行不通,只能重新追踪旋转代码的执行流程,却意外的发现这里有个很不一样的地方,在 Debug 环境下,如果我们通过 setOrientation
去修改App的朝向时,当Invocation生效之后,相关VC页面的 supportedInterfaceOrientations
代码都会被重新调用,然而在 Release 环境下,这一切都没有发生,而supportedInterfaceOrientations
是页面进行横竖屏刷新的必须的方法,那我们有理由相信,在 Release 条件下调用 Invocation 后,VC 的页面认为并不需要进行 UI 刷新,从而导致了我们的界面没有旋转。
这里回头再思考之前的代码分析,在 Release 和 Debug 都正确的通过 bl 调用了强制旋转的 invocation 后,产生了完全不一样的效果,那么猜测原因大概率在于,系统在 Release 环境下,针对 setOrientation
方法的实现做了优化,当 App 先通过旋转到 Orientation 朝向A之后,如果再进行 setOrientation: Orientation A
时,系统便会认为不必再进行刷新了,逻辑上也很容易理解,但是和我们的实现却发生了冲突。
解决问题
问题找到了,那解决的方案就很简单了。
最优解
优化旋转的处理逻辑,这里自动旋转时,应该是能够直接处理,而应该通过手动进行修改变量后再刷新,但现实是代码的依赖较多,不能激进的解决问题,短期内无法解决目前的Bug。
退而求其次
通过延迟执行 Invocation,让系统产生错觉,绕过优化,参考代码:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC))
{
... Invocation Code ...
}
禁止优化
其实一切根源在于系统在 Release 环境下的优化产生了和 Debug 环境不一样的结果预期,那么如果禁用掉优化,是不是就可以绕过这个问题了。这时候想到了 LLVM 本身有很多编译参数,通过 https://releases.llvm.org/3.8.0/tools/clang/docs/AttributeReference.html , 找了一圈,发现了一个叫做 optnone 的参数,具体说明如下:
The optnone attribute suppresses essentially all optimizations on a function or method, regardless of the optimization level applied to the compilation unit as a whole ...
看起来这个似乎正是我们需要的,针对 rotateIncovationWithOrientaion
进行配置后,发现一切都按照预期进行,旋转都恢复正常。最终代码:
static inline void rotateIncovationWithOrientaion(UIInterfaceOrientation orientation) __attribute__((optnone));
static inline void rotateIncovationWithOrientaion(UIInterfaceOrientation orientation) {
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
BOOL isLandscape = (orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight);
delegate.orientationType = isLandscape ? QINOrientationTypeLand : QINOrientationTypePor;
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = (int)orientation;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
总结
问题的根本还是需要对旋转的实现方案进行改进,现阶段只是通过取巧的方案来解决目前的问题。这个事情,也可以再次验证了,苹果在很多地方默默的做了非常多的优化来提升效率。