游戏线上测试总是有一些很奇怪的crash信息上报,闪退点是Unity引擎C++层的方法GameObject::GetSupportedMessagesRecalculate
。我们自己平时跑游戏,偶尔也会在场景切换的时候发生闪退。经过初步分析,确定是同一个crash。虽然收集到的闪退率不高,但既然我们自己人都碰到了,那线上实际情况可能会更容易出。
结论很简单,想看结论,直接跳到末尾即可。分析过程很坎坷,断断续续跨了有两三个月。分析过程分为两个阶段,阶段一主要是围绕崩溃点本身进行的分析,没有得出结论;阶段二,是在编辑器中复现出来的另外一种情况,最终找到了突破点。
阶段一
简略crash堆栈
从名字上猜测,是资源加载出来的时候出了问题,很可能是资源损坏了。
GameObject::GetSupportedMessagesRecalculate()
GameObject::SetSupportedMessagesDirty()
MonoBehaviour::AwakeFromLoad(AwakeFromLoadMode)
AwakeFromLoadQueue::PersistentManagerAwakeSingleObject(Object&, AwakeFromLoadMode)
TimeSliceAwakeFromLoadQueue::IntegrateTimeSliced(int)
PreloadManager::UpdatePreloadingSingleStep(PreloadManager::UpdatePreloadingFlags, int)
PreloadManager::UpdatePreloading()
详细crash信息
所幸在开发环境下,复现了一次,拿到了比较详细的堆栈信息。
E/CRASH: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***Version '2019.4.16f1 (e05b6e02d63e)', Build type 'Development', Scripting Backend 'mono', CPU 'armeabi-v7a'Build fingerprint: 'OPPO/R9s/R9s:6.0.1/MMB29M/1528528402:user/release-keys'Revision: '0'ABI: 'arm'Timestamp: 2021-08-13 12:39:01+0800pid: 18030, tid: 18096, name: UnityMain >>> com.stormx.test <<<uid: 10458signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x4Cause: null pointer dereferencer0 00000000 r1 00000000 r2 00000003 r3 f36cf930r4 d9668370 r5 00000000 r6 d7ac5b20 r7 d744b760r8 fd3ed0b0 r9 00000003 r10 0001fcf2 r11 00000001
E/CRASH: ip f36cfab8 sp f36cefb8 lr dacd9ba3 pc dacda05ebacktrace:#00 pc 0040f05e /data/app/com.stormx.test-2/lib/arm/libunity.so (GameObject::GetSupportedMessagesRecalculate()+18) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#01 pc 0040eb9f /data/app/com.stormx.test-2/lib/arm/libunity.so (GameObject::SetSupportedMessagesDirty()+22) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#02 pc 0086b70f /data/app/com.stormx.test-2/lib/arm/libunity.so (MonoBehaviour::AwakeFromLoad(AwakeFromLoadMode)+14) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#03 pc 008ad579 /data/app/com.stormx.test-2/lib/arm/libunity.so (AwakeFromLoadQueue::PersistentManagerAwakeSingleObject(Object&, AwakeFromLoadMode)+32) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#04 pc 0089ed43 /data/app/com.stormx.test-2/lib/arm/libunity.so (PersistentManager::IntegrateObjectAndUnlockIntegrationMutexInternal(int)+24) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#05 pc 006d3c11 /data/app/com.stormx.test-2/lib/arm/libunity.so (TimeSliceAwakeFromLoadQueue::IntegrateTimeSliced(int)+320) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#06 pc 006d52e9 /data/app/com.stormx.test-2/lib/arm/libunity.so (PreloadManager::UpdatePreloadingSingleStep(PreloadManager::UpdatePreloadingFlags, int)+80) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#07 pc 006d5915 /data/app/com.stormx.test-2/lib/arm/libunity.so (PreloadManager::UpdatePreloading()+180) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)
E/CRASH: #08 pc 006c95bb /data/app/com.stormx.test-2/lib/arm/libunity.so (InitPlayerLoopCallbacks()::EarlyUpdateUpdatePreloadingRegistrator::Forward()+38) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#09 pc 006c2b13 /data/app/com.stormx.test-2/lib/arm/libunity.so (ExecutePlayerLoop(NativePlayerLoopSystem*)+52) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#10 pc 006c2b47 /data/app/com.stormx.test-2/lib/arm/libunity.so (ExecutePlayerLoop(NativePlayerLoopSystem*)+104) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#11 pc 006c2cf9 /data/app/com.stormx.test-2/lib/arm/libunity.so (PlayerLoop()+264) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#12 pc 008d16a3 /data/app/com.stormx.test-2/lib/arm/libunity.so (UnityPlayerLoop()+490) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#13 pc 008f3fd5 /data/app/com.stormx.test-2/lib/arm/libunity.so (nativeRender(_JNIEnv*, _jobject*)+40) (BuildId: 3efcb2d01629f3876c8f81f15aad592efc75b1af)#14 pc 00592481 /data/app/com.stormx.test-2/oat/arm/base.odex (boolean com.unity3d.player.UnityPlayer.nativeRender()+76)
可疑日志
崩溃前,一段可疑的日志。说明崩溃前有过资源释放操作。
D/Unity: System memory in use before: 105.3 MB.
D/Unity: System memory in use after: 100.9 MB.Unloading 13317 unused Assets to reduce memory usage. Loaded Objects now: 8653.Total: 205.532813 ms (FindLiveObjects: 6.525573 ms CreateObjectMapping: 6.495416 ms MarkObjects: 159.178958 ms DeleteObjects: 33.328802 ms)
I/CrashReport-Native: Register backup native handler
源码
不要问我代码是哪里来的,总之有一份旧版的代码可以参考。从源码上看不出任何问题,不知道崩溃的行数,不好定位。只能反编译看看。
void GameObject::SetSupportedMessagesDirty()
{Assert(!IsDestroying());MessageIdentifier::OptimizedMessageMask oldSupportedMessage = m_SupportedMessages;m_SupportedMessages = 0;if (IsDestroying())return;GetSupportedMessagesRecalculate();if (oldSupportedMessage != m_SupportedMessages){for (Container::iterator i = m_Component.begin(); i != m_Component.end(); ++i)if (i->GetComponentPtr())i->GetComponentPtr()->SupportedMessagesDidChange(m_SupportedMessages);}
}
void GameObject::GetSupportedMessagesRecalculate()
{Assert(!IsDestroying());m_SupportedMessages = 0;for (Container::iterator i = m_Component.begin(); i != m_Component.end(); ++i)if (i->GetComponentPtr()) // !crash!m_SupportedMessages |= i->GetComponentPtr()->CalculateSupportedMessages();
}
反汇编
用IDA反编译一下libunity.so。 这个库位于Unity安装目录的Editor\Data\PlaybackEngines\AndroidPlayer\Variations
目录中,如果android打包是mono debug模式, 为mono\Development\Libs\armeabi-v7a\libunity.so
;如果是il2cpp debug模式,为il2cpp\Development\Libs\armeabi-v7a\libunity.so
;如果是release版本,把路径中的Development换成Release;如果是64位模式,把路径中的armeabi-v7a换成arm64-v8a。
对汇编不熟悉,只能边查资料,结合源码来分析。从crash的位置能够定位到发生闪退的指令位置为: #00 pc 0040f05e
, 为了方便解读,以下反编译代码顺序略有调整:
.text:0040F04C ; _DWORD GameObject::GetSupportedMessagesRecalculate(GameObject *__hidden this)
.text:0040F04C _ZN10GameObject31GetSupportedMessagesRecalculateEv
.text:0040F04C ; CODE XREF: GameObject::SetSupportedMessagesDirty(void)+16↑p
.text:0040F04C ; __unwind {
.text:0040F04C PUSH {R4,R5,R7,LR}
.text:0040F04E LDR R2, [R0,#0x3C] // r2 = m_Component.size(). r2 == 3, 有三个组件
.text:0040F052 LDR R1, [R0,#0x2C] // r1 = m_Component.begin()
.text:0040F050 MOV R4, R0 // r4 = r0 = this
.text:0040F054 MOVS R0, #0
.text:0040F058 STR R0, [R4,#0x50] // m_SupportedMessages = 0;
.text:0040F056 CMP R2, #0 // 判断m_Component.size() 是否等于 0
.text:0040F05A BEQ locret_40F07C // if == 0 goto locret_40F07C
.text:0040F05C MOV R5, R1 // Container::iterator i = m_Component.begin()
.text:0040F05E
.text:0040F05E loc_40F05E ; CODE XREF: GameObject::GetSupportedMessagesRecalculate(void)+2E↓j
.text:0040F05E !crash! LDR R0, [R5,#4] // component = i->GetComponentPtr()
.text:0040F060 CBZ R0, loc_40F072 //if (i == nullptr) goto loc_40F072
.text:0040F062 LDR R1, [R0]
.text:0040F064 LDR R1, [R1,#0x58] // r1 = i->GetComponentPtr()->CalculateSupportedMessages
.text:0040F066 BLX R1 // call CalculateSupportedMessages()
.text:0040F068 LDR R1, [R4,#0x2C] // r1 = this->m_Component.begin()
.text:0040F06A LDR R2, [R4,#0x3C] // r2 = this->m_Component.size()
.text:0040F06C LDR R3, [R4,#0x50] // r3 = this->m_SupportedMessages
.text:0040F06E ORRS R0, R3 // ret |= this->m_SupportedMessages
.text:0040F070 STR R0, [R4,#0x50] // this->m_SupportedMessages = ret
.text:0040F072
.text:0040F072 loc_40F072 ; CODE XREF: GameObject::GetSupportedMessagesRecalculate(void)+14↑j
.text:0040F072 ADD.W R0, R1, R2,LSL#3 // r0 = r1 + r2 << 3 = end = begin + size * 8
.text:0040F076 ADDS R5, #8 // ++i
.text:0040F078 CMP R5, R0
.text:0040F07A BNE loc_40F05E
.text:0040F07C
.text:0040F07C locret_40F07C ; CODE XREF: GameObject::GetSupportedMessagesRecalculate(void)+E↑j
.text:0040F07C POP {R4,R5,R7,PC}
.text:0040F07C ; } // starts at 40F04C
主要指令说明:
指令名字 | 英文解释 | 描述 |
---|---|---|
LDR | load memory data into register. | 把内存数据加载到寄存器中 |
STR | store register into memory. | 把寄存器的数据,写入到内存中 |
CMP | compare | 比较两个操作数,将结果写到状态寄存器的标记位中 |
B | branch(jump) | 跳转到目标地址 |
BEQ | branch(jump) if equal. | 如果状态寄存器的比较标志位的值是0,则跳转 |
BNE | branch(jump) if not equa. | 与BEQ相反 |
CBZ | compare branch(jump) if zero. | 如果寄存器的值为零,则跳转。不修改状态寄存器。 |
BL | branch with link | 用于函数调用的跳转 |
BLX | Branch with Link and exchange instruction set | 用于函数调用的跳转,并且切换指令集 |
分析
崩溃位置是对迭代器解引用(component = i->GetComponentPtr())的时候发生的,根据寄存器r5的值来看,此时i为NULL。有下面两种情况,会导致i为NULL:
- 假设
m_Component.begin()
为空,则迭代器i会是空。此时m_Component.size()
也应该是0,则for循环压根就不会进入。说明假设不成立; - 假设
m_Component.begin()
不为空,则迭代器i不会是空,i只有++操作,不可能变成空。
也就是说,i无论如何都不可能是空值。那就说名有可能出现了内存错误:
- 当前的GameObject已经被销毁了!此时this指针就是非法地址,理论上说,
执行this->m_SupportedMessages = 0
这一步时就会出现崩溃。当然,崩溃信息也不一定完全准确,而且两行条指令相邻,极有可能发生。 - 多线程问题。指令0040F04E和0040F052之间被多线程操作打断,别的地方销毁了m_Components。
中间就隔了一条之类,这种情况理论上概率极低。
分析到此为止,陷入了僵局,无法继续推进。只能猜测是某个资源损坏了,但是一直没发定位到是哪个资源。在网上搜索了下,也没有太多案例可以参考。
阶段二
很长一段时间后,就想着用编辑来模拟一下bundle的运行情况,看看能不能获得更详细的报错信息。经过若干次测试,终于在某个特定的情况下切换场景,碰到了大量的错误日志。并且编辑器停止游戏运行的时候,编辑器发生了闪退。
编辑器闪退堆栈:
========== OUTPUTTING STACK TRACE ==================0x00007FF7A53FE8A4 (Unity) GameObject::GetComponentIndex
0x00007FF7A5C8804E (Unity) CanReplaceComponent
0x00007FF7A5C87B50 (Unity) CanDestroyObject
0x00007FF7A5C8ADDF (Unity) DestroyObjectHighLevel
0x00007FF7A5CA08D3 (Unity) DestroyWorldObjects
0x00007FF7A45992ED (Unity) EditorSceneManager::RestoreSceneBackups
0x00007FF7A3FEE82E (Unity) PlayerLoopController::ExitPlayMode
0x00007FF7A4000CCF (Unity) PlayerLoopController::SetIsPlaying
0x00007FF7A40039A2 (Unity) Application::TickTimer
0x00007FF7A49874E5 (Unity) MainMessageLoop
0x00007FF7A49916C8 (Unity) WinMain
0x00007FF7A7A06962 (Unity) __scrt_common_main_seh
0x00007FFB875F7034 (KERNEL32) BaseThreadInitThunk
0x00007FFB88642651 (ntdll) RtlUserThreadStart========== END OF STACKTRACE ===========
编辑器的闪退堆栈没有太大价值,因为是在停止播放时发生的,而不是在出错位置。但是从堆栈上可以猜测出是某个GameObject或Component发生了野指针,导致销毁的时候引起了闪退。
编辑器使用bundle模式运行,收集到的错误日志:
Component at index 0 could not be loaded when loading game object 'Bip001'. Removing it!
(Filename: C:\buildslave\unity\build\Runtime/BaseClasses/GameObject.cpp Line: 811)Transform component could not be found on game object. Adding one!
(Filename: C:\buildslave\unity\build\Runtime/BaseClasses/GameObject.cpp Line: 741)Prefab has multiple Transform components! Removing them automatically would not be safe.
(Filename: C:\buildslave\unity\build\Runtime/BaseClasses/GameObject.cpp Line: 890)CheckConsistency: GameObject does not reference component Transform. Fixing.
(Filename: C:\buildslave\unity\build\Runtime/BaseClasses/GameObject.cpp Line: 1394)
而错误日志也是让人很困惑,没有指明是哪个资源出了问题。即便我把含有’Bip001’的所有结点全部删掉,又会出现另外一些结点出错。在网上查了一下,有相似的问题,都是资源损坏引起的:
- prefab在版本合并时,出现了合并混乱,导致prefab格式被破坏;
- 资源是旧版Unity生成的,升级Unity后资源格式需要升级,或者bundle需要重新生成;
- prefab中含有丢失的内嵌预设(Missing Prefab);
- 资源中含有丢失的脚本(Missing Script);
- CacheServer中资源发生了损坏;
- Library缓存目录中的资源发生了损坏。
用脚本扫描了所有的资源,确实出现很多损坏问题。把资源问题逐一修复后,删除了所有缓存,重新打bundle,结果还是一样,失望ing。
不过,至此可以排除是资源损坏的问题。回到出问题的地方,刚好是切换场景,那最有可能的就是某个资源正在异步加载或对象在创建的过程中,被切换场景给销毁了。Unity创建对象的接口只有Instantiate,而且实例化对象是同步的。那就只可能资源在异步加载的过程中,bundle被Unload引起了异常。查了下资源加载器代码,果然在异步加载资源的时候,没有对bundle增加引用计数,导致切换场景的时候被释放掉了。至于Unity为何没有拦截掉这种错误的用法,就不得而知了。
清除Missing Script
GameObjectUtility.RemoveMonoBehavioursWithMissingScript(GameObject go);
查找内嵌的Missing Prefab
static void FindMissingPrefab(GameObject go, string name, bool isRoot, bool recursive = true)
{if (go.name.Contains("Missing Prefab")){Debug.LogError($"1. {name} has missing prefab {go.name}", go);return;}if (PrefabUtility.IsPrefabAssetMissing(go)){Debug.LogError($"2. {name} has missing prefab {go.name}", go);return;}if (PrefabUtility.IsDisconnectedFromPrefabAsset(go)){Debug.LogError($"3. {name} has missing prefab {go.name}", go);return;}if (!isRoot){if (PrefabUtility.IsAnyPrefabInstanceRoot(go)){return;}GameObject prefabRoot = PrefabUtility.GetNearestPrefabInstanceRoot(go);if (prefabRoot == go){return;}}if (recursive){name = name + "/" + go.name;foreach (Transform child in go.transform){FindMissingPrefab(child.gameObject, name, false, recursive);}}
}
总结
卸载正在异步加载资源的AssetBundle,会导致Unity引擎内部出现指针错误,引发一些奇怪的闪退问题。
经过此次闪退分析,基本上可以确定,堆栈含有MonoBehaviour::AwakeFromLoad(AwakeFromLoadMode)
,都是资源损坏引起的。可能是资源真的有问题,或AssetBundle损坏了,或资源正在加载过程中AssetBundle被释放了。