记一次 Release 版本旋转失效问题

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];
}

总结

问题的根本还是需要对旋转的实现方案进行改进,现阶段只是通过取巧的方案来解决目前的问题。这个事情,也可以再次验证了,苹果在很多地方默默的做了非常多的优化来提升效率。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容