专栏首页逍遥剑客的游戏开发Android平台上的Native内存分析

Android平台上的Native内存分析

背景

UE4游戏在Android上的进程内存占用(PSS)很让人困惑, 没有一个清晰直观的方式可以统计到每一部分的内存占用. 所以在做内存分析的过程中顺手做了一个统计工具, 可以从系统底层统计UE4在Android的所有内存分配(包括Graphics部分).

UE4的内存统计

UE4本身提供了3种内存分析方式:

下面分别做一下说明

memreport

游戏中console command输入”memreport-full”可以保存内存报告在Saved/Profiling/MemReports目录下, 就是一个文本文件, 长这样:

里面有各种类型的内存统计数据, 如Texture, RTT, VB/IB, Particle, Component等, 每一种都有比较详细的信息. 缺点是引擎只会从某个角度做统计, 对于一些统计代码没有覆盖到的内存是没有相关信息的, 而且各个统计类型之间是有重叠, 无法把各个子项加起来就能还原出内存的总体占用分布.

MemoryProfiler2

引擎代码中打开USE_MALLOC_PROFILER宏, 编译后使用”mprof start/stop/snapshot”命令来dump内存日志, 再使用MemoryProfiler2.exe打开查看:

MemoryProfiler2的统计是基于UE4的内存分配器, 通过记录每次内存操作的调用堆栈, 然后进行累加统计, 使用GUI工具按照树形结构显示出来, 能够非常直观地进行分析. 缺点是会影响内存分配的效率, 而且记录的信息量太大, 中低端Android手机很难保存成功, 抓取到后工具的分析效率也比较慢. 如果一些内存没有通过UE4的内存分配器进行分配的话, 那是统计不到的, 比如一些第三方库或者组件.

LLM

LLM是4.18才加入的功能, 使用-LLM命令行参数启动才生效, 当时只有PC和主机平台可以用, 游戏中使用”stat LLM/LLMFULL”打开实时查看:

这种统计方式是基于scope的方式, 类似CPU性能分析的原理, 执行某段代码前记录一下, 执行后再统计对比一下, 这样可以做到没有遗漏, 而且也不会影响游戏的执行性能. 缺点是只是按模块或者分类的统计, 没有更细节的信息指导优化.

Android进程内存

使用dumpsys meminfo命令可以查看Android进程的内存占用信息:

adb shell dumpsys meminfo <package_name|pid> [-d]

一般输出的信息像这样:

基中Gfv dev, EGL mtrack, GL mtrack是新版本Android才加入的, 之前版本都被统计在Native Heap中. 对比memreport就会发现, Unknown部分基本上与UE4自身统计到的内存一致, 那是因为UE4底层调用了mmap/munmap来进行内存分配, 所以没有被系统统计到Native Heap中:

FMalloc* FAndroidPlatformMemory::BaseAllocator()
{
	const FPlatformMemoryConstants& MemoryConstants = FPlatformMemory::GetConstants();
	// 1 << FMath::CeilLogTwo(MemoryConstants.TotalPhysical) should really be FMath::RoundUpToPowerOfTwo,
	// but that overflows to 0 when MemoryConstants.TotalPhysical is close to 4GB, since CeilLogTwo returns 32
	// this then causes the MemoryLimit to be 0 and crashing the app
	uint64 MemoryLimit = FMath::Min<uint64>( uint64(1) << FMath::CeilLogTwo(MemoryConstants.TotalPhysical), 0x100000000);

#if PLATFORM_ANDROID_ARM64
	// todo: track down why FMallocBinned is failing on ARM64
	return new FMallocAnsi();
#else
	// [RCL] 2017-03-06 FIXME: perhaps BinnedPageSize should be used here, but leaving this change to the Android platform owner.
	return new FMallocBinned(MemoryConstants.PageSize, MemoryLimit);
#endif
}

void* FAndroidPlatformMemory::BinnedAllocFromOS(SIZE_T Size)
{
	void* Ptr = mmap(nullptr, Size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
	LLM(FLowLevelMemTracker::Get().OnLowLevelAlloc(ELLMTracker::Platform, Ptr, Size));
	return Ptr;
}

如果把FMallocBinned替换成FMallocAnsi, 那么所有的内存都会被统计进Native Heap, 这时就可以使用Android的Malloc Debug进行内存分析了:

DDMSMemoryAnalyzer

注: 这个方法只在MiPad(Android4.4)上测试过, 最新版本Android的DDMS已经没有这个功能了.
  • 首先保证/system/lib/libc_malloc_debug_leak.so存在, 如果没有, 需要自行提取相应版本并拷贝过去
  • 然后setprop libc.debug.malloc 1, 开启系统级的Malloc Debug(也可以指定进程)
  • C:\Users[yourname].android\ddms.cfg, 加一行””native=true”
  • 启动ddms.bat, “Snapshot Current Native Heap Usage”, 可以抓取一个带有调用堆栈地址的txt文件

有了堆栈地址, 我们可以通过arm-linux-androideabi-addr2line.exe把地址翻译成函数名. 由于txt中保存的是内存地址, 而addr2line需要的是so的相对地址, 所以我们需要获取libUE.so的内存地址, 做个减法, 才能使用addr2line进行翻译. 使用以下命令可以找到libUE4.so的内存地址:

adb shell pmap -x [PID]

比如下图, libUE4.so的内存地址是70226000:

有了so的相对地址, 就可以使用addr2line进行翻译了:

arm-linux-androideabi-addr2line.exe -C -s -f -e libUE4.so [ADDRESS]

但是txt中记录的是每次调用的堆栈, 次数非常多, 每一次的分配大小都比较小, 不具备实际优化的参考价值, 所以把所有的堆栈地址合并进行内存的累加统计, 以树形结构自上而下查看, 内存占用就变得非常直观了, 这就是DDMSMemoryAnalyzer的基本思想:

同样的方法, 也适用于Unity引擎:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • MPQ文件系统优化

    逍遥剑客
  • 被FMOD的内存管理坑了一把

    逍遥剑客
  • Direct3D资源

    逍遥剑客
  • JavaScript内存泄漏了解

    程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

    javascript.shop
  • JavaScript 内存泄漏教程

    一、什么是内存泄漏? 程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。 对于持续运行的服务进程(daemon),必须及...

    ruanyf
  • [操作系统]内存覆盖与交换

    思想: 1)将程序分为多个段,常用的段常驻内存,不常用的段需要时调入内存 2)内存分为一个"固定区",若干个"覆盖区" 3)需要常驻的放在"固定区",调入后不在...

    陶士涵
  • Linux 内存管理初探

    linux 内存是后台开发人员,需要深入了解的计算机资源。合理的使用内存,有助于提升机器的性能和稳定性。本文主要介绍 linux 内存组织结构和页面布局,内存碎...

    用户6543014
  • Java架构师中的内存溢出和内存泄露是什么?实际操作案例!

    申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 521M 的内存一直不回收,那么可以用的内存只有 521M 了,仿佛泄露掉了一部分;

    Java架构师进阶技术
  • 软件性能测试(连载9)

    Linux内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。Linux的空间又分为内核空间和用户空间,在32位中,内核空间占1G,用户空间...

    小老鼠
  • Linux之《荒岛余生》(三)内存篇

    内存问题,脑瓜疼脑瓜疼。脑瓜疼的意思,就是脑袋运算空间太小,撑的疼。本篇是《荒岛余生》系列第三篇,让人脑瓜疼的内存篇。其余参见:

    xjjdog

扫码关注云+社区

领取腾讯云代金券