騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

Qling發表於2020-04-03
編者按: 手遊佔用手機記憶體過大,會影響玩家的體驗。騰訊遊戲學院專家Qling將在本文分享自己做Android記憶體優化的思路,希望能幫助到大家。

作者: Qling 騰訊互動娛樂 遊戲客戶端開發

在之前Android客戶端記憶體優化工作中發現,Android的記憶體組成部分較多,而每一部分的含義以及測量工具在官方文件以及Google中都沒有找到詳細資料,最終通過分析相關Android原始碼以及測試對每部分含義有了一定了解,所以分享出來為同樣做記憶體優化工作的同學提供一定思路,少走彎路。

01 認知

個人覺得在做記憶體的優化前,先樹立一個正確的認知是非常必要的,這樣可以避免鑽牛角尖,少做很多無用功。目前總結出兩點認知如下:

測不準

在做優化工作時,大家必定要做的事就是先看看當前數值是多少,優化過之後再和優化前數值做對比,所以優化前要做的第一件事就是測量。而對記憶體而言,卻很難精確測量某一時刻或者某個情景下當前的記憶體是多少,同樣條件下每次測量的結果可能都會有一定浮動,所以不要太糾結上來就先去測一個準確值出來。其實從文章後面的內容也能知道,衡量Android記憶體的一些指標本身的定義就不是一個精確值。


三個誤區

  • 轉場景記憶體有增量
  • 一段時間內一直增長
  • 進出場景Unity Profiler回落正常,但Android記憶體沒有完全回落


優化工作中可能會經常碰到上述三種情況,很多時候可能會覺得發生了記憶體洩露,但其實也並不一定。轉場景記憶體有增量不一定是記憶體洩露,只要保證在Unity Profiler裡看到的Texture、Mesh以及SerializedFile(AssetBundle)等常見易洩露的資源解除安裝乾淨即可。一般情況下,Android或iOS並不會及時將所有App解除安裝資料進行清理,為了保證下次使用時的流暢性,OS會將部分資料放入到快取,待自身記憶體不足時,OS Kernel會啟動類似LowMemoryKiller的機制來查詢快取甚至殺死一些程式來釋放記憶體。Unity Profiler回落但Android記憶體沒有回落是因為Profiler裡記錄的是引擎真實使用的記憶體,而Android中的記憶體大小是包含了部分快取。因此,並不能通過一兩次的記憶體沒有完全回落來說明記憶體洩露問題。

02思維導圖

簡單總結了一下在記憶體優化時的思維順序。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

文章接下來也會按照這樣的順序進行展開。


03指標

要優化,首先必須要量化,要量化就需要選取指標,個人覺得在指標的選取上需要滿足以下幾個條件:

  • 易測量
  • 符合邏輯
  • 每次測量結果穩定,方差小


而對於Android的記憶體,其實已經有一些大家常用的衡量指標了,分別是USS、PSS、RSS和VSS,這幾個指標的具體含義相信做記憶體優化的同學都很清楚,就不贅述了。在實際專案中一般會選擇PSS作為衡量指標。

04工具

確定指標後,要做的就是通過一定的工具來測量相應指標。Android本身提供了非常多用於測量記憶體的工具,如free、showmap、procrank等,可以按照不同的需求採用不同的工具。對於PSS,個人覺得最方便易用的工具是dumpsys meminfo,用法如下:

adb shell dumpsys meminfo --package com.tencent.xxx

執行結果如下:

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

其中PrivateDirty列和Private Clean列是程式獨佔的總記憶體,不和其它程式共享。程式銷燬時,它們佔用的記憶體會重新釋放回系統。Dirty記憶體是已經被修改的記憶體頁,Clean記憶體則是沒有被修改的記憶體頁(例如正在執行的程式碼)。右側的Heap Alloc列指應用中Dalvik堆和本地堆已經分配使用的大小。它的值比Pss Total,因為Android中所有程式都是從Zygote中fork出來的,包含了程式共享的部分。

其餘每行的含義會在後續詳細講解,每行指標也都會有相應的工具檢視。

05主要

通過工具得到具體數值後,接下來要做的就是優化了。而優化時需要採取的策略就是分清主要矛盾和次要矛盾,即找到佔用記憶體的大頭。在Android記憶體中,瓶頸主要來自上圖中的Native、Gfx、Unknown三項。文章接下來會對這三項做詳細解釋。

Native

一個Android程式的記憶體從high-level層面講,可以粗略分為Java堆和Native堆,其中Natvie堆顧名思義就是由C/C++等分配的記憶體,對Unity專案而言,一般即為Unity引擎申請的記憶體。通過DDMS工具抓到的Native記憶體如下圖所示:

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

可以看到Native記憶體主要由libunity.so、libmono.so、libglsl.so以及公司的libapollo.so和libGCloudVoice.so等動態庫組成。而Native記憶體中的內容一般包含以下幾個:

  • Mesh
  • Font
  • Fmod
  • Texture(R/W)
  • Material/Shader
  • Animation Clip等


對於Native記憶體的檢視,網上的資料都是介紹利用DDMS工具,但坦白講,覺得這個工具的設計初衷就只是為Java服務的,所以要想用它來檢視Native記憶體,需要非常非常複雜的配置流程,當時為了配好工具差不多花了三四天時間,走了很多彎路,而且網上的資料都不可行,最終還是通過看Android相關原始碼才搞定。只是DDMS的配置差不多都可以單獨寫一篇文章來介紹了,所以不再次詳述。

其實在配置好DDMS後,會發現它顯示的內容實在是太多了,目前看到的結果是除了Texture和Font等大物件外,其餘分配都是由n個很小的分配組成的條目,並不是我們想象中有一個10MB的AnimationClip在Native裡就會對應於一個10MB的分配。在XCode裡檢視iOS版,結果也是一樣。因此除了像Texture以及Font這類物件的記憶體外,想借助DDMS排查其他物件的記憶體問題幾乎是不可能的。

所以自己實現了一個小工具,通過使用Unity提供的Android底層物件封裝,利用反射呼叫Android底層介面,得到實時Native、Gfx以及Unknown值。可以在不同模組點插入一些Sample得到兩個Sample之間的記憶體變化量,進而在一個high-level層面查出記憶體增長不正常的地方,雖然工具很簡單,但已幫助解決了很多Native記憶體問題。工具結果和相關程式碼片段如下圖所示。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

這裡也有一個經驗之談,如果把從工具定位問題再到程式碼裡優化看做是一個自底向上的過程的話,當自底向上行不通時(如DDMS資料太多)就可以考慮自頂向下的方式,從業務邏輯模組出發,慢慢定位到問題所在。

Native記憶體一個常見的問題是紋理開了Read/Write Enable,當紋理沒有開啟可讀寫時只會在Gfx中存在一份,但開啟可讀寫後就會在Native記憶體中也存在一份。如下圖所示:

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

可以發現開啟可讀寫後,DDMS看到的Native記憶體裡有一項Texture2D::Transfer的分配。

視訊記憶體

Android上的視訊記憶體分為Gfx和GL兩部分,其中Gfx指使用者態視訊記憶體,內容包含貼圖和Mesh,使用Unity Memory Profiler即可檢視。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

沒找到移動端底層相關資料,可以藉助上圖理解使用者態視訊記憶體,猜測原理近似(如有理解錯誤還請大神不吝賜教)。上圖是Windows Display Driver Mode(WDDM)的結構,以D3D為例,顯示卡驅動和D3D執行時都分為使用者態和核心態。應用程式呼叫D3D API,執行在使用者態的D3D執行時經過UMD(Userspace Mode Driver)生成Command Buffer,然後再由執行在核心態的D3D執行時和核心態的驅動處理相應buffer,交給GPU繪製。所以猜測移動端的Gfx即為使用者態顯示卡驅動使用的記憶體。

與Gfx相對應,GL則是指核心態的視訊記憶體,包含Texture、Vertex Buffer等。但GL指標在很多裝置上並不會顯示,這是因為這塊視訊記憶體一般是由GPU使用的,其大小需要由晶片廠商自己計算。Android提供了一個memtrack模組,如果晶片廠商實現了該模組,且Android系統版本在7.0以上,則可以通過dumpsys meminfo得到該指標。下圖是高通的一個實現:

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

也可以通過命令

cat /d/kgsl/proc/pid/mem

來檢視GL記憶體中的內容,如下圖所示。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

檢視高通完整的實現程式碼可以知道,高通在計算GL大小時已經剔除了Gfx中的記憶體,所以在高通的架構下,Android中視訊記憶體整體大小應該為Gfx+GL。

關於GL視訊記憶體需要額外說明的是Unity從5.x版本開始,就包含一個由於申請VBO導致的GL視訊記憶體過大的bug,目前該bug在5.6.3p2和2017.1.0p5中修復。對於Unity空專案,GL視訊記憶體會從修復前的50MB降為20MB。

在優化貼圖記憶體方面,本人也做了另外一份壓縮貼圖合併的工作。有的時候我們需要在執行時把一些壓縮過的小貼圖合併到一張大的Atlas中,Unity的Texture2D有一個PackTextures()的介面,但這個介面只有在小貼圖是DXT1格式時,合成的Atlas也是DXT1。對於常用的ETC、PVRTC等貼圖,合併出來的Atlas是RGBA32格式的,這樣明顯會增大記憶體。所以自己實現了一個合併壓縮貼圖的外掛,外掛支援幾乎所有壓縮格式的貼圖合併,支援Mipmap,也支援Android、iOS和x64等各種平臺。如果有同學需要,可以聯絡我。壓縮格式的相關資料可以參看[2][3][4]。

Unknown

Unknown記憶體一般是Mono堆記憶體和Lua記憶體(如果專案中使用了Lua),Mono堆記憶體分配也可以通過Unity Memory Profiler檢視,但需要將指令碼引擎設定為IL2CPP。檢視Lua記憶體也有相關Profiler可以使用。

建立一個空指令碼,加入以下程式碼申請16MB記憶體,

byte[] b = newbyte[1<<24];

此時的Unknown記憶體如下圖所示(申請前約為2MB)。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

06次要

對整體PSS貢獻不多的次要內容主要有Dalvik和EGL兩項。

Dalvik

Dalvik是Java虛擬機器使用的堆記憶體,一般是由Java申請的,可以使用DDMS中的工具進行詳細檢視(如下圖所示),也可以通過其他工具如MAT等進行分析。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

這一項一般較小且很穩定,所以可以忽略。對於公司專案而言,該項記憶體主要是由Apollo、MSDK等外掛佔用,目前大小為17MB。

EGL

EGL具體指EGLSurface,是由Android的SurfaceFlinger子系統(類似於Windows中的DWM(Desktop Window Manager))使用,用於將GPU中渲染的結果最終顯示在螢幕上。下圖很好的解釋了SurfaceFlinger的作用,可以把它理解為一個合成器,它的輸入可以來自不同程式,比如Launcher、NavigationBar和StatusBar分別屬於不同的系統服務。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?


Android從4.4開始使用三緩衝區(Tripple-buffering),使用三緩衝區的原因可以在網上找到相關資料,不再贅述。Android採用EGLSurface作為一個Back Buffer,所以當App正在前臺執行時,EGL記憶體大小為3個螢幕大小的Back Buffer,當App執行在後臺不顯示時EGL為一個螢幕大小的Back Buffer。具體Back Buffer大小與螢幕解析度相關,如1080p的螢幕(1080x1920 RGBA32)的大小約為8MB。EGL同樣也包含非常多的內容,感興趣的同學可以檢視[7]中的電子書。

可以通過

cat /d/kgsl/proc/pid/mem

檢視某一個App使用的EGLSurface個數,同檢視GL記憶體的指令相同,如下圖(左)所示。也可以通過

dumpsys surfaceflinger

檢視系統整體的EGLSurface情況,如下圖所示。

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?

關於EGL想額外提到的一個很玄學的點是,經過多次試驗發現如果Plugins/Android/AndroidManifest.xml中設定了<uses-sdk標籤,那麼EGL記憶體就會翻倍。這個問題也跟Unity工作人員討論過,但無果。由於用AndroidStudio建立的空專案不存在該問題,所以仍然懷疑是Unity的bug。

介紹完EGL,至此就可以對Android的顯示有一個全域性的認識,App呼叫相應3D API讓GPU把內容繪製到EGLSurface中,這時SurfaceFlinger將EGLSurface以及Navigation Bar和Status Bar等做合成,然後顯示在螢幕上。可能有的同學會問遊戲一般都是全屏的,是不需要Navigation Bar或Status Bar的,但其實在遊戲中從螢幕邊緣向下或者向左右滑動時,仍然是會在遊戲介面上顯示Navigation Bar或Status Bar的。

其實在SurfaceFlinger到Screen之間,還有一個可選的模組HWC(Hardware Composer),用於最終把內容顯示到螢幕上。如果存在HWC,那麼SurfaceFlinger就只需要告訴HWC顯示哪些內容即可,無需關心如何顯示。HWC模組一般是由不同硬體廠商自己實現的,拿最簡單的例子講,不同機型的Navigation Bar實現都是不同的,有的廠商採用系統預設(軟體Navigation Bar),有的則是實體按鍵。

參考資料
[1] https://source.android.com/devices/graphics/
[2] https://www.imgtec.com/blog/pvrtc-the-most-efficient-texture-compression-standard-for-the-mobile-graphics-world/
[3]https://www.khronos.org/registry/OpenGL/extensions/OES/OES_compressed_ETC1_RGB8_texture.txt
[4]https://www.khronos.org/assets/uploads/developers/library/2012-siggraph-opengl-es-bof/Ericsson-ETC2-SIGGRAPH_Aug12.pdf
[5] https://blog.uwa4d.com/archives/optimzation_memory_2.html
[6] https://developer.android.com/topic/performance/memory-overview.html
[7] https://mathias-garbe.de/files/introduction-android-graphics.pdf
[8] https://www.jianshu.com/p/59ad90bff2a7
[9] http://djt.qq.com/article/view/987
[10] https://source.android.com/devices/graphics/implement-vsync?hl=zh-cn

關於騰訊遊戲學院專家團


如果你的遊戲也富有想法充滿創意,如果你的團隊現在也遇到了一些開發瓶頸,那麼歡迎你來聯絡我們。騰訊遊戲學院聚集了騰訊及行業內策劃、美術、程式等領域的遊戲專家,我們將為全世界的創意遊戲團隊提供專業的技術指導和遊戲調優建議,解決團隊在開發過程中遇到的一系列問題。


申請專家資源請前往:
https://gwb.tencent.com/cn/tutor


來源:騰訊GWB遊戲無界
原地址:https://mp.weixin.qq.com/s/t194bj5K5d5NluHrAYNfeg

相關文章