ue5导入玛雅模型崩溃是什么情况
UE4/UE5的崩溃,卡死等问题处理
quabqi
UE4/UE5 游戏开发
来自专栏手摇虚幻引擎
虚幻引擎的业务逻辑开发基本上都是用C++/蓝图,当因为项目代码写的不好遇到Crash等问题时,如果不了解Native程序和引擎底层的一些机制,相比用C#开发业务的Unity或其他完全基于脚本虚拟机的游戏确实要难处理一些。因为业务和引擎代码本身都是基于C++,所以对于解决常规C++的Crash的方法虚幻引擎完全适用,除此外引擎在异常处理上相比于普通的C++程序还是提供了一些额外的方法和工具。本文主要介绍虚幻引擎在处理Crash时的一些做法和经验技巧。
常规崩溃定位
当游戏崩溃时,对于开发来说肯定是希望能定位到哪行代码崩了,发生崩溃当时的内存是什么样的,在虚幻引擎里这个工作是引擎自动做的。引擎会将崩溃的dump文件保存在Saved/Crashes/下面,编辑器的位置如下图
运行时的游戏包的位置也类似,PC版就是在游戏目录,安卓在Android/data/(游戏包名)/下面,iOS就在app对应的Documents目录下。当有多次崩溃时,可以自己按照修改日期排序,找最新的即可
打开后可以看到有这么多信息。
dmp文件:这个文件崩溃的dump信息,可以把这个文件直接拖到visual studio里,就会自动跳到崩溃现场的那一行代码上。
log文件:这个文件就是崩溃时的log信息,可以根据打出的日志做一些崩溃辅助判断。比如在崩溃之前做了哪些关键操作。
runtime-xml文件:这个文件用文本记录了崩溃时的现场,包括堆栈,崩溃的代码等,本质上和dmp文件差不多,因为dmp是二进制文件并不可读,在手上没有符号文件时,这个文件可以用于分析崩溃。如下图可以看到
了解了引擎的这一个功能,基本上80%的崩溃都可以定位出来。像是最常见的Signal 11,靠这个就初步能查到现场是哪里。
卡死检测
有时候我们很难根据崩溃的现场查到是什么原因崩溃的想在一些关键位置输出堆栈或内存等信息。或者不一定是崩溃,而是死循环卡死了,那么肯定不会有上面这样的dump信息输出。引擎接入了Lua或其他脚本语言,想在脚本出异常时,肯定也有想要顺便输出一下C++堆栈的情况。因此肯定还是希望能够自己有一些办法在代码里主动输出当前的堆栈。引擎也提供了这样的方法,可以见StackWalk类:
这里有一系列的函数,比如可以通过StackWalkAndDump函数将当前的堆栈输出到字符串里。当然如果不是GameThread,这个类也提供了dump指定线程的堆栈。比如lua脚本里的代码崩溃了,但因为lua的崩溃有一个通用函数兜底,C++肯定不会直接崩,我们这时就可以手动调用这样的函数,将C++的堆栈写到log里。
对于业务卡死,虚幻引擎也封装了一个单独的守护线程ThreadHeartBeat,当检测到某个线程的心跳超时时,内部也是调用上面的函数将卡死的线程堆栈输出到log里,如下图。
当然自己的业务也可以仿照上面的做法封装。这个功能默认是关闭的,毕竟有额外线程的开销,需要项目自己根据情况把USE_HANG_DETECTION宏打开。
内存随机崩溃或泄漏
内存写坏,程序随机崩溃的这个问题,我想应该是大多数项目最苦恼的问题了。其实虚幻底层也对解决这些问题提供了一些辅助定位的代码。因为本身是内存问题,因此这些工具代码也是在内存上下功夫的。
我们知道虚幻本身有在全局重载C++的new和delete,在业务分配和释放内存时,实际调用的是引擎的FMemory类中的Malloc和Free。而引擎会根据情况从内存池去获取内存。而内存池本身的内存,是引擎根据时机去向系统要内存。如果你用过引擎的LLM统计内存,应该就知道LLM有两个Tracker,Default和Platform,这两个口径也就对应业务向引擎要内存和引擎向操作系统要内存这两种情况。
这张图片来源于网络,如侵权请告知删除
其中LLM Default和LLM Platform就如下图所示的关系。我们平常一直说UE4/UE5的项目不要使用STL也是因为这个机制。因为STL内部有自己的allocator,在没有指定allocator时所有的内存分配都不受引擎管理,而且因为STL本身只有头文件,即使明显指定了allocator,在跨dll使用时也可能因为疏忽造成一些内存问题。
这里重点是FMemory内部可以使用多种分配器,且有的分配器是可以嵌套的,对于上层业务来说无感知的,引擎默认一般会使用Binned2或Binned3,内部会按照size做内存池,而内存池不够时,每次向系统申请的都是固定大小的Chunk。因为本文重点不是讲内存管理以及LLM,就不过多展开了。
为了查内存写坏的问题,就需要在这里打开一些特殊的分配器。最常用的就是下面几个:
Ansi:这个是标准的分配器,也就是让UE4不使用任何额外的内存管理,就直接走平台原生的new和delete,有时候需要用到平台的一些内存工具,开到这个模式会非常好。比如在iOS平台上需要查内存泄漏问题,如果使用默认的Binned2/Binned3,那么用Xcode自带的Instruments肯定查不到泄漏的具体代码在哪,看到的都是内存池在申请,而开到Ansi就可以定位到内存泄漏的现场。
Stomp:这是引擎提供的查内存写坏的利器,一般开了这个模式,崩溃时的第一现场就是内存写坏的代码位置。具体原理是利用了操作系统的虚拟地址这个概念,我们知道向系统要内存时,拿到的指针其实只是一个虚拟地址,真正是否分配了物理内存是会根据情况来决定的。可以看下图注释,能解决这几种问题: