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:
- 假设
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被释放了。
Unity资源加载闪退问题深度分析相关推荐
- Unity资源加载发布到移动端iphone/ipad
Unity资源加载发布到iOS平台的特殊路径 using UnityEngine; using System.Collections; public class TestLoad : MonoBeha ...
- Unity资源加载入门
写在前面 本文转载自:https://gameinstitute.qq.com/community/detail/123460,供自己学习用,如有疑问,请移步原创. 引言 Unity的资源加载及管理, ...
- Unity资源加载管理
转载链接: https://bbs.gameres.com/thread_800362_1_1.html 我理解的资源管理 举一个不恰当的例子来描述我所理解的资源管理(因为我实在想不出更合适的例子了) ...
- Unity资源加载方式
一.Unity特殊资源目录 Resources:逻辑资源目录,这个目录中的资源会打入到包中,不允许热更.在打包时会被压缩和加密. 加载方式:Resources.Load(常用) . AssetData ...
- android游戏加载,Android 游戏引擎libgdx 资源加载进度百分比显示案例分析
因为案例比较简单,所以简单用AndroidApplication -> Game -> Stage 搭建框架 一.主入口,无特殊 public class App extends Andr ...
- Unity资源加载简析(一)Resources
一.Resources(此类允许按照路径名来查找并加载物体) 1.Resources.Load加载 加载储存在Resources文件夹中path处的资源(Resouces文件夹可以在Assets文件夹 ...
- rust加载闪退_腐蚀Rust游戏崩溃怎么解决 腐蚀Rust游戏崩溃处理方法-游侠网
腐蚀Rust有时候玩家会遇到游戏崩溃的情况,导致游戏无法正常的运行,但是很多玩家都不清楚怎么解决这一错误,今天小编为大家带来"Newki"分享的腐蚀Rust游戏崩溃处理方法,希望能 ...
- unity 异步加载网络图片_一个非常好用的AssetBundle资源加载器
Loxodon Framework Bundle是一个非常好用的AssetBundle加载器,也是一个AssetBundle冗余分析工具.它能够自动管理AssetBundle之间复杂的依赖关系,它通过 ...
- 【Unity3D日常开发】Unity中的资源加载与文件路径
推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 QQ群:1040082875 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有 ...
- Unity游戏开发——新发教你做游戏(三):3种资源加载方式
文章目录 一.前言 二.Unity的目录结构规范 1.Resources(不是很推荐把资源放这个目录) 2.RawAssets(存放生资源) 3.GameRes(存放熟资源) 4.StreamingA ...
最新文章
- php使用workerman实战,使用workerman实现在线聊天的方法
- python运行不了程序代码_python怎么运行代码程序
- mysql command line client 目标不对_简单几招提高MySQL安全性
- 如何获取 SAP Commerce Cloud Spartacus UI 购物车 Cart 的加载状态
- Linux系统学习:目录结构和文件管理指令
- 互联网上,极致才能成功
- 6月第4周全球域名注册商(国际域名)新增注册量TOP22
- [windows]JDK安装与环境变量配置
- 华为服务器装系统怎么选pxe,服务器设置pxe启动
- 青春互撩——详解基于Socket通信的聊天软件开发(附项目源码)
- 一份Slide两张表格带你快速了解目标检测
- JQuery中$(document)是什么意思?
- Processing 案例 | 去“富士山”看樱花从树上纷纷而落
- JAVA学习笔记(第五章 接口与继承)
- 诸神之战|福建赛区圆满收官,IP“论剑”引爆现场
- Android面试经验一:
- Apache Calcite 论文翻译
- 【Bootstrap-学习小结】
- 程序员2天做出的猫咪情绪识别软件,究竟用了什么技术?
- 《程序员的自我修养》读书笔记——动态链接