Unity资源加载闪退问题深度分析
游戲線上測試總是有一些很奇怪的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:
也就是說,i無論如何都不可能是空值。那就說名有可能出現了內存錯誤:
執行this->m_SupportedMessages = 0這一步時就會出現崩潰。當然,崩潰信息也不一定完全準確,而且兩行條指令相鄰,極有可能發生。
中間就隔了一條之類,這種情況理論上概率極低。
分析到此為止,陷入了僵局,無法繼續推進。只能猜測是某個資源損壞了,但是一直沒發定位到是哪個資源。在網上搜索了下,也沒有太多案例可以參考。
階段二
很長一段時間后,就想著用編輯來模擬一下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被釋放了。
總結
以上是生活随笔為你收集整理的Unity资源加载闪退问题深度分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 流水线、超流水线、超标量(supersc
- 下一篇: 软件测试---------兼容性 / 安