展示其他SKView后SKView.presentScene带转场效果失效问题排查
更新
该问题由存在间接影响的循环引用(retain cycle)引发,完整的问题诊断过程已整理成文档记录。
我遇到了一个非常特殊的bug,表现为在其他UIView中展示过SKView后会产生副作用,看起来是旧iOS9版本中presentScene带转场失效bug的复现。
背景
Touchgram是一款iMessage应用扩展,支持创建和播放交互式消息,类似PowerPoint和游戏编辑器的结合体,基于消息体系运行。其界面采用UIKit构建,播放能力基于SpriteKit实现。
Touchgram引擎会创建SKScene对象,在多页面消息中可选择带转场或不带转场展示这些场景。
编辑界面初始为树状视图,可打开详情编辑页修改触摸检测、页面跳转动作、音效播放等配置。
部分详情编辑页也会展示SKView:其中触摸编辑器的特定模式下会展示SKView,用于录制bounding area(边界区域)或待匹配的手势;文本元素编辑器会展示SKView,用于预览样式对小段文本的影响,会在页面主背景(纯色或图片)上渲染文本元素。
问题描述
正常流程:启动应用(实际是iMessage内的应用扩展)后播放已保存的消息,所有功能正常,场景转场可以正常展示。
异常情况:只要进入文本元素编辑页,哪怕其内嵌的SKView没有绘制任何内容,后续的播放功能就会被破坏。此时播放任何带转场的消息都会卡住,前一个场景的节点会持续展示不消失。
我已经将问题范围缩小到该页面上SKView的存在本身:如果仅移除该SKView,问题就不会出现。当前该SKView甚至没有绑定输出口,也没有任何代码和它交互。
另一个触摸编辑页也包含SKView,但不会产生该副作用。
我花了数天才定位到该副作用,直到看到相关技术讨论帖才意识到,bug表现不一致是因为部分消息包含场景转场、部分不包含。该问题已经阻塞了我们历时一年多开发的核心引擎的首次产品发布。
正常页面 - 触摸编辑器
xib中视图层级的相关代码片段如下:
<objects> <view opaque="NO" contentMode="scaleToFill" id="Mia-xp-RE9"> <rect key="frame" x="0.0" y="0.0" width="414" height="736"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <subviews> ... <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rlf-kc-zvh" userLabel="RecordingOverlay"> <rect key="frame" x="0.0" y="0.0" width="414" height="736"/> <subviews> ... <skView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="AnC-CG-Aqq" userLabel="RecordingArea"> <rect key="frame" x="57" y="191" width="300" height="543"/> <constraints> <constraint firstAttribute="width" secondItem="AnC-CG-Aqq" secondAttribute="height" multiplier="414:750" priority="950" id="zMg-Iv-5nu"/> </constraints> </skView> </subviews> ... </view> </subviews> ... </view>
异常页面 - 文本编辑器
xib中视图层级的相关代码片段如下:
<objects> <view opaque="NO" contentMode="scaleToFill" id="upw-ro-x1m"> <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="KTl-Xs-TIo" userLabel="MainContentView"> <rect key="frame" x="0.0" y="44" width="375" height="734"/> <subviews> ... <skView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Wch-3x-CLz" userLabel="preview"> <rect key="frame" x="91" y="307.33333333333326" width="193" height="349.66666666666674"/> <constraints> <constraint firstAttribute="width" secondItem="Wch-3x-CLz" secondAttribute="height" multiplier="414:750" priority="950" id="jeK-un-SXq"/> </constraints> </skView> ... </subviews> ... </view> </subviews> ... </view>
排查方向
我尝试找出两个页面的差异,以及触发该UIKit/SpriteKit异常行为的原因,以下是公开的思路梳理,无前置假设,仅排查正常/异常页面的差异带来的副作用,大部分选项看起来影响概率极低,但仍在逐一确认:
- 正常页面存在相邻/重叠的scrollView:正常页面有一个scrollView和另一个视图,会根据不同模式切换显隐。
- 排除
异常页面是UIViewController子类,之前绑定SKView时将其绑定到了父类的输出口。 - 排除
异常页面是SKSceneDelegate,在viewDidLoad中被设为代理,但是代理是通过可选值清空的,可能存在未清空的情况。 - 排除
存在其他VC代理混合:异常页面实现了第三方富文本字体选择的代理协议,同时是UIText相关代理。 - 排除
skView的嵌套层级问题:目前正常页面的SKView嵌套层级比异常页面更深,将SKView上移到和"MainContentView"平级也没有解决问题。 - 内存释放:在正常和异常页面中添加
deinit方法,确认返回主树状视图后、播放前两个页面是否都被销毁。最终找到差异! - 异常页面从未调用deinit。
已排除的无关因素
以下因素基本确认对问题无影响:
- 是否绑定输出口:调试初期异常页面的SKView是绑定了输出口的,解绑后问题仍然存在。
- 是否可见:两个页面的SKView初始都是隐藏的,通过代码控制显示,无论是否显示都会触发问题。
- SKView是否有渲染内容:在SKView未绑定输出口、无任何代码交互的情况下,仅xib中存在该SKView就会触发副作用。
内容的提问来源于stack exchange,提问作者Andy Dent

