1. 程式人生 > >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引擎:
在這裡插入圖片描述