UE5安卓移动端内存分析手段简介及使用案例分享
UE5作为一个复杂庞大的引擎,用它制作出的游戏,如果不加注意,往往在制作的过程中一不小心就超过了目标平台的硬件性能限制。今年我们能在PC、主机等高端平台上见到不少优化存在一定问题的UE5游戏,高端平台尚且如此,那么针对移动端平台的游戏,性能优化就显得更为重要了。内存就是性能指标中非常关键的一项,降低游戏运行时的内存占用,对游戏最终的性能表现和稳定性影响很大。
本文就将介绍一些UE5安卓移动端内存分析手段,它们各自的作用,我们在使用过程中遇到的踩坑点,以及使用案例;抛砖引玉,欢迎大佬批评指正!
以下所称UE5均指文章撰写时的UE5.1版本
分析手段
我们大致将内存占用的分析分为三个颗粒度:系统级、应用级、资产级,分别对应着三套不同的内存分析手段。
这也对应着一个大致的分析流程:先宏观再微观,先从整体掌握内存大致占用,再到应用内寻找内存压力点,最后再根据压力点找到对应引发问题的资产或逻辑。
系统级
我们主要使用adb命令来查看系统级别的内存占用信息。其中最常用的命令是 adb shell dumpsys meminfo,这个命令可以生成指定应用在当时的内存占用快照,供我们交叉验证数据的可靠性和有效作非常具有参考价值;此外还有一些其他命令,可以提供更多数据
meminfo
以下是一个示例项目的内存占用情况:
能见到它包含了许多内存统计的细项和简要的归纳总结。我们可以把主要的关注点放在第一列的Pss Total上,它指的是Proportional Set Size,是进程实际占用的物理内存大小。但是android内存涉及到在系统中同其他进程共享一部分(可能是so,可能是字体文件等的mmap),所以这里面会考虑这个因素计算你的进程平摊的这部分共享库的大小,这里的p就意味着统计了这个平摊后的物理内存。这是衡量进程占用内存的比较可信的指标。官方文档对PSS统计的评价如下:
PSS衡量的一个特点是,您可以将所有进程的PSS加起来确定所有进程占用的实际内存。这表示PSS是一种理想的方式,可用来衡量进程的实际 RAM占用比重,以及相对于其他进程和可用的总 RAM而言,对RAM的占用情况。
由于我们只是需要一个宏观的大致内存组成,我们可以主要关注App Summary的部分,这是meminfo对上面细分项的内存占用的一些简单的归纳,对于我们了解项目的大致内存足够用了。其中又比较重要的是:
- Native Heap:主要包括一些第三方库的内存占用和图形API的driver
- Graphics:显存部分,包括Buffer、Shader、Texture、RenderTarget等在GPU侧的内存
- Code:UE项目本身的代码大小
- Private Other:主要为UE通过FMalloc分配出去的内存
其中,我们可以在UE应用层对Graphics和Private Other部分的内存进行更详细的拆解。
其他指令
-
adb shell top
实时显示系统中的各个应用内存占用,主要观察RES
-
adb shell dumpsys procstats
procstats 可以让您了解应用在一段时间内的表现,包括应用在后台运行的时长以及在该期间内的内存占用情况。它可以帮助您快速找到应用中的低效环节和不当行为(如内存泄漏),这些问题可能会影响应用的表现,特别是在低内存设备上运行时。其状态转储会显示有关每个应用的运行时间、
按比例分摊的内存大小(PSS)、独占内存大小(USS)和常驻内存大小(RSS)等统计信息。这个命令的主要优势在于可以看到USS。内存显示的顺序为
(minPSS-avgPSS-maxPSS/minUSS-avgUSS-maxUSS/minRSS-avgRSS-maxRSS)
应用级
Insights
UE自身也带有许多趁手的内存分析命令行和工具,迭代了不少版本。在最新的UE5中官方最推荐的性能测试工具是Unreal Insights,其中的Memory Insights可以实时追踪项目的内存占用,实时生成曲线图,并能看到相当一部分的各系统内存占比。关于Unreal Insights本身如何使用可以直接参考官方文档。相比系统级别的内存快照,Memory Insights能让我们看到更详细的内存分布,比如音频系统的内存占用是多少,Shader文件的占用是多少,lua占用的又是多少,等等等等。UE5的Memory Insights甚至支持检查内存泄漏,可惜的是该功能在移动端真机调试时不可用。
示例项目在1 min内的Insights大致曲线如下:
以时间为横轴,纵轴分别是各个统计项下的内存占用情况,通过曲线图我们可以很清晰地看到整个项目内存占用随着时间的变化,对我们定位内存峰值、压力点,以及排查可能的内存泄露问题等都能起到帮助。比较重要的是需要厘清各个Tag之间的大小级关系和覆盖范围,避免出现重复统计的问题。
我们筛选出比较值得关注的Tag是:
ProgramSize
内存统计初始化之前的内存占用,应和meminfo中Code部分的大小大致相同
FMalloc
大致大小对应meminfo中的Private Other部分,为UE通过FMalloc分配出去给各个子系统使用的内存,其下有很多细分Tag,其中值得关注的一些Tag是:
-
Textures (CPU端,不包含实际纹理数据)
-
Shaders (Shader源码大小,CPU端)
-
Physics(物理系统)
-
Meshes (CPU数据,不包含GPU的mesh数据)
- StaticMesh
- SkeletalMesh
- lnstanceMesh
-
UI
-
Ul_Style
-
UI_Texture(不包含实际的GPU侧texture数据)
-
Ul_Text(字体)
-
UI_UMG
-
Ul_Slate
-
-
Navigation(寻路)
-
Filesystem(文件系统,IO相关)
-
Niagara(特效)
-
Animation(不包含实际Animation数据)
-
Audio(音效)
-
Engine Misc
-
UObject(其他所有没有被打tag,继承自UObject的部分)
-
Lua
-
ConfigSystem(ini配置文件?)
-
Localization(本地化)
-
FMallocUnused(FMalloc申请了但没用上的)
-
Overhead(内存统计工具自身占用)
GPU方面
- VulkanDriverMemoryGPU(估算的总GPU内存占用)
- Textures(纹理)
VulkanTextures / TextureMemory2D +3D + Cube - RenderTartgets
VulkanRenderTargets / Rendertarget memory 2d - VulkanShader
- VulkanBuffers(Mesh数据)
- Textures(纹理)
LLM
Memory Insights底层使用的统计工具是虚幻的LLM(Low Level Memory Tracker),通过插入各种tag来将所有待统计的内存划归到某个tag下。它通过维护一个tag的堆栈,将Fmalloc的内存统计到当前栈顶的tag下。LLM在最底层hook了fmalloc的每一个统计,如果没有任何tag在当前栈中,那么所有内存计入在untagged这个tag下,如果我们在代码中插入一个基于scoped的tag,就可以把这个scope下的内存计入你的tag下。
通过LLM我们不会遗漏任何Fmalloc分配的内存,这部分的内存就归属于系统级中提到的PrivateOther部分的内存;此外,LLM也追踪到了Fmalloc之外的内存,包括LLM工具启动前就已经占用了的内存大小、工具本身的内存大小等等;还有一些内存占用能统计到占用,但LLM不知道用途,只能统一归到Untracked部分;最后在Vulkan API下,LLM还估算了GPU端的一些内存占用,也可起到一定程度的参考作用。
LLM内存信息除了通过Memory Insights查看,也可以通过启动时的附加命令行 -llm -llmfull
等命令打开,在游戏内使用stat llm stat llmfull stat llmplatform
等实时查看内存信息;还可以附加-llmcsv
命令,游戏会每5秒钟保存一次llm信息到一个csv文件中,可以使用UE自带的csv分析工具转换成可读的html文件查看,有兴趣的可以自行搜寻教程。
其他命令行
除了Insights和LLM,UE还有一些其他的命令行可以给我们提供一些额外的内存信息,比如
-
stat streaming:显示纹理流送相关的内存占用情况,有价值的是能看到正在流送的纹理和无法流送的纹理内存占用分别是多少
-
Used Streaming Pool(正在流送)
-
Unstreamable Render Assets / NonStreaming Mips
-
-
stat rhi:显示RHI相关的资源占用,比如Render Target、Texture等,这些数据可以和LLM估算的Vulkan相关数据互相印证
资产级
具体分析到资产级别的内存占用,我们可用的手段不多,目前最常使用的是UE自带的内存快照memreport。在游戏内打开控制台,输入memreport -full -csv(可选)
,可以生成当前时刻的内存快照,它与使用Insights等工具统计内存最大的不同是它可以将内存占用项的统计颗粒度细化到资产级别,方便我们对具体资产的内存占用进行分析。当我们利用之前的工具定位到一些内存压力点可能是资产带来的问题(比如,过大的纹理占用),我们就可以使用memreport查看具体的内存占用分布,寻找出问题的资产。
一份完整的memreport(使用-full)包括以下值得关注的数据:
- 当时的所有stat统计数据,包括LLM、streaming、rhi等等
- 所有继承自UObject的类的实例,将它们按在内存中占用的大小,从大到小排列
- Skeletal Mesh、Static Mesh资产的详细内存占用
- 纹理资产的详细内存占用,条件包括非VT的、未压缩的、所有纹理
- Render Target的详细内存分布,各张rt的实际大小
- 以Component形式存在的Mesh的大小(比如Instance)
一份memreport文件根据项目的大小和资产数量,有可能非常长,UE有自带的工具可以分析一部分数据,但是想要从中拆分出其他想要的数据就可能需要自己写工具来解析csv文件了。
踩坑点
数据可靠性
不同的统计方法对同一项内存数据的表述可能不同,数据也可能略有不同,比如meminfo中的Private Other部分和Insights中的FMalloc部分。我们需要确认哪些数据是能对上的,从而确定数据的有效性。经过测试我们认为可靠的数据已经在上文中列出。
机型差异
由于系统版本、处理器型号等等的差异,不同的安卓机型可能在同一个项目上可能出现同一个统计数据相差很大的情况;我们使用的测试机小米10S(骁龙870,安卓11)、红米K60Ultra(天玑9200+,安卓13)和华为P20(麒麟970,安卓10)就在一些关键数据上出现了较大的差异。我们用以下原则来确定数据有效性:
- 少数服从多数,优先以不同机型之间数据相似的为准,剔除不一样的个例
- 数据对齐原则,以不同统计方法之间更加能对齐的数据为准
BUG
LLM工具中对Vulkan相关的统计有明显的bug,将所有的统计数据都归在VulkanDriverMemoryCPU下,细分项全部为0,起不到参考作用。这个问题已在我们的引擎中被修复。
案例分享
案例1:可能的内存泄露
在使用Insights正常跑图测试中观察到FMalloc内存持续上升而不下降的情况,增长点在RequestLevel之后
排查后发现关卡的卸载机制出了问题,导致已加载到内存的关卡没有被正确卸载,进行了修复。
案例2:减少显存占用
在memreport中观察到Non-Streaming的纹理内存占用偏高,是Streaming纹理的三倍以上
排查详细纹理资产内存占用,发现大批UI纹理未进行纹理压缩,同时不进行Texture Streaming导致Non-Streaming的纹理占用偏高
对UI统一处理成4的倍数后,压缩UI纹理,维持Non Streaming,降低了Non-Streaming的纹理内存占用。