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

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


編者按 手遊佔用手機內存過大,會影響玩家的體驗。騰訊遊戲學院專家Qling將在本文分享自己做Android內存優化的思路,希望能幫助到大家。


文 | Qling

騰訊互動娛樂 遊戲客戶端開發


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


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


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


測不準


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


三個誤區


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


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


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

簡單總結了一下在內存優化時的思維順序。


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

(點擊上圖,可放大查看)


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


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

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


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


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


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

確定指標後,要做的就是通過一定的工具來測量相應指標。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出來的,包含了進程共享的部分。


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


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

通過工具得到具體數值後,接下來要做的就是優化了。而優化時需要採取的策略就是分清主要矛盾和次要矛盾,即找到佔用內存的大頭。在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內存優化?


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


對整體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,至此就可以對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

"/<uses-sdk>


分享到:


相關文章: