记一个SceneKit Morpher引发的Crash
tags: AR&3D
SceneKit
背景
Animoji外网遇到一些Crash, 量不算大但一直存在,由于测试很难复现,只能靠看Log归纳用户的操作路径,对可疑点进行排查。
分析
猜测
外网Crash的堆栈顶都是C3DGeometryGetMesh,进入C3DGeometryGetMesh大多是C3DMorpherUpdateIfNeed或者C3DMorpher的其他方法,但是再往后看调用者,就特别随机了……有时是在更新贴图,有时是在更新表情,甚至是挂在渲染时钟里,没有任何业务代码。
C3D开头的类多是SceneKit、SpriteKit或ModelI/O的底层实现, 从类名结合具体业务逻辑,可以断定和SCNMorpher有关。
Animoji是使用SCNMorpher做表情动画的,当用户选择不同Animoji模型,我会根据优先级加载不同表情,从这里入手开始查起。
定位原因
首先我先取消了分批加载表情的策略,在创建模型时就完成加载完表情再去渲染,结果灰度用户还是Crash……
之后分析了业务逻辑和Log, 觉得有可能SCNMohpher不是线程安全的,因为Morpher的创建是异步的。
猜测苹果的实现是异步加载具体网格的顶点数据到自己的Targets的数组,在更新时使用Metal计算每个顶点的变形后位置。
尝试复现
基于上面的分析,首先写了一个时钟,30帧调用选择不同Animoji的接口,果然Crash了,栈的结构与外网的类似。
再次分析
从Crash现场和寄存器的来看,Morpher要加载某一个网格时,这个网格的数据是空的,所以Crash了,佐证了我之前的判断。但是如果Morpher这么不安全,那么这个Bug早就应该大面积爆发了,而且Morpher的接口设计也没有加载完成的回调。
在Radar bug给苹果后,继续分析这个Bug的成因,毕竟项目还是要上线的。
分析业务代码,发现Animoji在更换模型时,是重复利用一个Node来操作,更换时只是替换这个Node的Geometry和Morpher等,再控制Node旋转移动等。
由于Morpher是附在Node上的,猜测苹果的实现,Node上可能有Morpher的信息,再一次做实验,用时钟30帧一次更换模型,这一次更换时会重新创建Node,将旧的Node从渲染场景中剥离,并在更新表情时判断Morpher的Targets数量是不是合预期。
// MARK: - code doesn't crash
// Change Animoji
[self.fakerFace removeFromParentNode];
SCNNode *node = [SCNNode node];
SCNMorpher *morpher = [SCNMorpher new];
morpher.calculationMode = SCNMorpherCalculationModeNormalized;
morpher.targets = arr;
morpher.unifiesNormals = YES;
return morpher;
node.morpher = morpher;
self.fakerFace = node;
[self.headParentNode addChildNode:self.fakerFace];
//MARK: - In Render Loop
if (self.fakerFace.morpher.targets > ANIMOJI_BLENDS_COUNT){
// Update Animoji
}
实验结果表明不会Crash了,就算以60调用也没有问题。
解决
修改代码后,外网已经没有这个Crash了,修复这个Bug主要靠猜苹果的实现……
也给大家分享下这个坑,使用Morpher要注意它的异步创建特点,同时要更换Morpher时要换个Node去持有它,不要直接加载到正在渲染循环中的Node,同时要判断Targets是否符合预期。不然底层代码会在取网格数据是碰到空值,造成Crash.