IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity资源加载闪退问题深度分析 -> 正文阅读

[游戏开发]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+0800
    pid: 18030, tid: 18096, name: UnityMain  >>> com.stormx.test <<<
    uid: 10458
    signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x4
    Cause: null pointer dereference
        r0  00000000  r1  00000000  r2  00000003  r3  f36cf930
        r4  d9668370  r5  00000000  r6  d7ac5b20  r7  d744b760
        r8  fd3ed0b0  r9  00000003  r10 0001fcf2  r11 00000001
E/CRASH:     ip  f36cfab8  sp  f36cefb8  lr  dacd9ba3  pc  dacda05e
    backtrace:
          #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

主要指令说明:

指令名字英文解释描述
LDRload memory data into register.把内存数据加载到寄存器中
STRstore register into memory.把寄存器的数据,写入到内存中
CMPcompare比较两个操作数,将结果写到状态寄存器的标记位中
Bbranch(jump)跳转到目标地址
BEQbranch(jump) if equal.如果状态寄存器的比较标志位的值是0,则跳转
BNEbranch(jump) if not equa.与BEQ相反
CBZcompare branch(jump) if zero.如果寄存器的值为零,则跳转。不修改状态寄存器。
BLbranch with link用于函数调用的跳转
BLXBranch with Link and exchange instruction set用于函数调用的跳转,并且切换指令集

分析

崩溃位置是对迭代器解引用(component = i->GetComponentPtr())的时候发生的,根据寄存器r5的值来看,此时i为NULL。有下面两种情况,会导致i为NULL:

  1. 假设m_Component.begin()为空,则迭代器i会是空。此时m_Component.size()也应该是0,则for循环压根就不会进入。说明假设不成立;
  2. 假设m_Component.begin()不为空,则迭代器i不会是空,i只有++操作,不可能变成空。

也就是说,i无论如何都不可能是空值。那就说名有可能出现了内存错误:

  1. 当前的GameObject已经被销毁了!此时this指针就是非法地址,理论上说,
    执行this->m_SupportedMessages = 0这一步时就会出现崩溃。当然,崩溃信息也不一定完全准确,而且两行条指令相邻,极有可能发生。
  2. 多线程问题。指令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被释放了。

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-09-11 19:08:27  更:2021-09-11 19:09:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/17 16:12:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码