游戏线上测试总是有一些很奇怪的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:

  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被释放了。

Unity资源加载闪退问题深度分析相关推荐

  1. Unity资源加载发布到移动端iphone/ipad

    Unity资源加载发布到iOS平台的特殊路径 using UnityEngine; using System.Collections; public class TestLoad : MonoBeha ...

  2. Unity资源加载入门

    写在前面 本文转载自:https://gameinstitute.qq.com/community/detail/123460,供自己学习用,如有疑问,请移步原创. 引言 Unity的资源加载及管理, ...

  3. Unity资源加载管理

    转载链接: https://bbs.gameres.com/thread_800362_1_1.html 我理解的资源管理 举一个不恰当的例子来描述我所理解的资源管理(因为我实在想不出更合适的例子了) ...

  4. Unity资源加载方式

    一.Unity特殊资源目录 Resources:逻辑资源目录,这个目录中的资源会打入到包中,不允许热更.在打包时会被压缩和加密. 加载方式:Resources.Load(常用) . AssetData ...

  5. android游戏加载,Android 游戏引擎libgdx 资源加载进度百分比显示案例分析

    因为案例比较简单,所以简单用AndroidApplication -> Game -> Stage 搭建框架 一.主入口,无特殊 public class App extends Andr ...

  6. Unity资源加载简析(一)Resources

    一.Resources(此类允许按照路径名来查找并加载物体) 1.Resources.Load加载 加载储存在Resources文件夹中path处的资源(Resouces文件夹可以在Assets文件夹 ...

  7. rust加载闪退_腐蚀Rust游戏崩溃怎么解决 腐蚀Rust游戏崩溃处理方法-游侠网

    腐蚀Rust有时候玩家会遇到游戏崩溃的情况,导致游戏无法正常的运行,但是很多玩家都不清楚怎么解决这一错误,今天小编为大家带来"Newki"分享的腐蚀Rust游戏崩溃处理方法,希望能 ...

  8. unity 异步加载网络图片_一个非常好用的AssetBundle资源加载器

    Loxodon Framework Bundle是一个非常好用的AssetBundle加载器,也是一个AssetBundle冗余分析工具.它能够自动管理AssetBundle之间复杂的依赖关系,它通过 ...

  9. 【Unity3D日常开发】Unity中的资源加载与文件路径

    推荐阅读 CSDN主页 GitHub开源地址 Unity3D插件分享 简书地址 我的个人博客 QQ群:1040082875 大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有 ...

  10. Unity游戏开发——新发教你做游戏(三):3种资源加载方式

    文章目录 一.前言 二.Unity的目录结构规范 1.Resources(不是很推荐把资源放这个目录) 2.RawAssets(存放生资源) 3.GameRes(存放熟资源) 4.StreamingA ...

最新文章

  1. php使用workerman实战,使用workerman实现在线聊天的方法
  2. python运行不了程序代码_python怎么运行代码程序
  3. mysql command line client 目标不对_简单几招提高MySQL安全性
  4. 如何获取 SAP Commerce Cloud Spartacus UI 购物车 Cart 的加载状态
  5. Linux系统学习:目录结构和文件管理指令
  6. 互联网上,极致才能成功
  7. 6月第4周全球域名注册商(国际域名)新增注册量TOP22
  8. [windows]JDK安装与环境变量配置
  9. 华为服务器装系统怎么选pxe,服务器设置pxe启动
  10. 青春互撩——详解基于Socket通信的聊天软件开发(附项目源码)
  11. 一份Slide两张表格带你快速了解目标检测
  12. JQuery中$(document)是什么意思?
  13. Processing 案例 | 去“富士山”看樱花从树上纷纷而落
  14. JAVA学习笔记(第五章 接口与继承)
  15. 诸神之战|福建赛区圆满收官,IP“论剑”引爆现场
  16. Android面试经验一:
  17. Apache Calcite 论文翻译
  18. 【Bootstrap-学习小结】
  19. 程序员2天做出的猫咪情绪识别软件,究竟用了什么技术?
  20. 《程序员的自我修养》读书笔记——动态链接

热门文章

  1. [IOS]——播放器AVPlayer的实现
  2. maven下载安装及配置
  3. OpenDRIVE:学习文档
  4. 3.2-点云配准原理概述
  5. 树莓派linux虚拟键盘,树莓派raspbian安装matchbox-keyboard虚拟键盘
  6. linux 内核专题— drv术语
  7. 系统分析师论文通用格式
  8. ARM 汇编语言教程
  9. CES直击:戴尔连发多款ALIENWARE与XPS新品
  10. 【github】-MM-Wiki初体验