記一個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.