分析並優化 Android 應用記憶體

image
00 前言
演講人介紹
Rechard Uhler,Android Runtime 開發工程師。為便於寫作,筆者將以第一人稱視角對視訊內容進行概述。
image

image
視訊地址:
https://www.youtube.com/watch?v=w7K0jio8afM&list=PLpvKQarSfUV0l-fMxTrJIDF3IvwoXlGOA
02 正文
想要進行記憶體優化,就必須對 Android 記憶體管理機制有比較深入的瞭解,這樣才能保證應用在低端機上也能有良好的表現。不同的記憶體型別,包括 Shared Memory,Dex Memory 以及 GPU Memory, 都會對使用者體驗產生影響。
我在過去的三年時間裡,都在致力於深入理解 Android 應用記憶體管理機制。那麼,為什麼 App 開發工程師也要關注記憶體佔用呢?於我而言,主要是因為 Android 生態系統。如果一個 Android 應用在低端裝置上使用者體驗不好(比如經常卡頓),那麼 OEM(Original Entrusted Manufacture) 就不願再生產這樣的裝置,進而導致這部分使用者被排除在 Android 生態系統之外。
本次課題主要討論三點內容:
-
低記憶體時 Android 系統的工作機制
-
如何評估應用記憶體使用情況
-
如何減少應用記憶體佔用
低記憶體時 Android 系統的工作機制
首先,需要介紹實體記憶體的概念,然後引入 Android Low Memory Killer。
-
實體記憶體
裝置的實體記憶體被分為很多頁(Page),每頁 4KB。不同的頁用來做不同的事情:

image
橘黃色的是已使用頁,黃色的是快取頁(資料在磁碟上有備份,所以 Cache Pages 是可以被回收的),綠色的是空閒頁。
用於回收 Cached pages 的 kswapd 程序
這是一個 2G 記憶體的手機,X 軸表示使用時間,Y 軸表示記憶體使用情況。隨著開啟的應用越來越多,Used Pages 也越來越多,而 Cached Pages 和 Free Pages 則越來越少。當 Free Pages 低於 kswapd 的閾值時,Linux 核心就會通過 kswapd 程序對 Cached Pages 進行回收。當應用再次訪問 Cached Pages 上的內容時,就需要從磁碟上重新載入。如果 Cached Pages 太少的話,裝置就可能宕機:

image
所以,在 Android 上我們有個機制叫 Low Memory Killer,當 Cached Pages 太少時,就會被觸發。它的工作方式是根據程序的優先順序,選擇性地殺死某個程序,釋放該程序佔用的所有資源以滿足記憶體分配需要:

image
如上圖所示,當 Cached Pages 低於 LMK 閾值時,將會觸發低記憶體殺死機制。
LMK(Low Memory Killer)
如果 LMK 殺掉的是使用者正在互動或可以感知的程序,將會導致非常不友好的使用者體驗。所以 Android SystemServer 程序維護了一張程序優先順序列表,LMK 根據這張表來決定先殺死哪個程序:

image
-
Perceptible 指的是非使用者直接互動的程序,比如在後臺播放音樂的音樂播放器程序;
-
Previous 指的是切換至當前前臺應用前的應用程序;
-
Cached 指快取的程序,這可能是退至後臺的應用程序,也可能是已經退出的應用程序,目的是為了實現應用間的快速切換。所以,Cached 程序也是優先順序最低的程序:

image
如上圖所示,當已用記憶體超過 LMK 閾值時,LMK 將從 Cached 列表底部開始殺死程序。如果可用記憶體還是不滿足分配需要,那麼將會按照上表所示優先順序自底向上殺死程序,直到準備 Kill SystemServer 程序,這將導致手機重啟。
所以,你可以想象 LMK 在低記憶體手機上的情景:

image
如上圖所示,LMK 將一直處於活躍狀態,具體表現就是應用卡頓、桌面黑屏重啟,手機宕機等等。如此,OEM 將不願生產這些裝置。
評估應用記憶體使用情況
那麼,我們怎麼知道 App 使用了多少記憶體呢?
-
實體記憶體追蹤
之前提到,裝置的實體記憶體被分為很多頁(Page),Linux Kernel 將會持續跟蹤每個程序使用的 Pages,所以只要對程序使用的 Pages 進行計數即可:

image
但實際情況遠比這要複雜的多,因為有些 Pages 是程序間共享的:

image
共享記憶體頁計數方法
RSS(Resident Set Size):App 完全負責

image
PSS(Proportional Set Size):App 按比例負責,比如下圖所示兩個程序共享,那就負責一半。如果三個程序共享,那就負責三分之一:

image
USS(Unique Set Size):App 無責:

image
但實際上,至少需要系統級別的上下文才能知道識別 RSS 與 USS。所以通常都是使用 PSS 來計算,這也可以避免多計或者少計 Shared Pages。你可以使用:
adb shell dumpsys meminfo -s [process]
命令來檢視一個程序的 PSS 使用情況:

image
最底部的 TOTAL 代表的就是應用按比例佔用的總記憶體大小。
應用記憶體佔用分析
如果想要應用支援的功能越多,UI 越炫酷,那就需要更多的記憶體分配。既想馬兒跑,又想馬兒不吃草的事情是不存在的:

image
- 記憶體佔用影響因素
應用使用場景:很好理解,哪個頁面比較炫、動效多、或者使用了 webview,那這個時候 App 佔用的記憶體就高:

image
平臺配置:很好理解,比如手機的解析度越高,相同 dp 的圖片佔用的記憶體就越大,所以高檔手機上,App 的記憶體佔用肯定比低檔手機高:

image
裝置記憶體壓力:裝置記憶體越緊張,越可能觸發 GC,導致 App 佔用記憶體比裝置記憶體充裕時低:

image
所以,你應當在相同的記憶體壓力下評估你的 App 記憶體佔用:

image
由於記憶體壓力不好控制,所以建議評估前,先一鍵清理所有程序,然後再測試。
減少應用記憶體佔用
使用 Android Studio 的 Memory Profiler,可以檢視當前 Java 堆上分配了哪些物件、物件大小以及物件引用鏈和被引用鏈等很多資訊。Live Allocation 中有 image heap、zygote heap、app heap 等可以選擇,但是我建議你只關注 app heap。因為 image heap 和 zygote heap 是 App 啟動時從系統繼承過來的,對於這部分記憶體佔用,我們基本上無能為力:

image
關於 Memory Profiler 的細節我不會講太多,因為明天中午 12:30 Esteban 將會詳細講解 Profiler 的用法,畢竟這是他們團隊開發的。所以,我強力推薦你們也參加一下明天的宣講會。
Java Heap 以外的記憶體佔用分析
上面提到,TOTAL 是 PSS,那麼這張圖中,除了 Java Heap,其它的是什麼意思呢?對於這部分記憶體佔用,我們又能做什麼呢?
image

image
這就比較好玩了,因為這部分大多是由 Android 平臺產生的,如果你真的想理解他們,那麼你需要學習很多專業知識。比如 Framework 是如何實現 View 系統及 Resource 管理的,Native Code 是如何執行的,WebView 是如何工作的,Android Runtime 是如何執行你的程式碼的,HAL 如何管理你的 Graphics 以及 Linux 核心的虛擬記憶體管理方式等等。
順便說一下,我生活在這兒,這個橘黃色的方塊裡(Android Runtime):
image

image
Android 平臺產生的記憶體佔用診斷
那麼,對於平臺產生的記憶體佔用,我們需要使用工具來診斷嗎?首先,我們可以使用:
adb shell dumpsys meminfo -a [process]
來檢視更詳細的資訊(以下資料為筆者自己開發的 App 的記憶體佔用情況):
Applications Memory Usage (in Kilobytes):Uptime: 498024399 Realtime: 1230430304** MEMINFO in pid 10898 [com.yuloran.wanandroid_java] **PssPssSharedPrivateSharedPrivateSwapPssHeapHeapHeapTotalCleanDirtyDirtyCleanCleanDirtySizeAllocFree------------------------------------------------------------Native Heap3582208243576432248740757763878636989Dalvik Heap40010304355272412240684734243423 Dalvik Other52560485256000Stack12004120000Ashmem13004128400Gfx dev2596002596000Other dev16010400160.so mmap23782221881132504133202218815.jar mmap680868000.apk mmap802924076841872240.ttf mmap2232000956200.dex mmap219741986402013080198640.oat mmap37764003620640.art mmap65474048685852758440424Other mmap40801286443760EGL mtrack246600024660000GL mtrack4524004524000Unknown213001842124000TOTAL14070242564349292860411844339239826234221040412 Dalvik Details.Heap3308003308000.LOS42016124284.LinearAlloc40200204020000.GC384016384000.JITCache59600596000.Zygote5830288164683840.NonMoving680068000.IndirectRef256012256000App SummaryPss(KB)------Java Heap:9808Native Heap:35764Code:50436Stack:120Graphics:31780Private Other:8344System:4450TOTAL:140702TOTAL SWAP PSS:39 ObjectsViews:207ViewRootImpl:1AppContexts:3Activities:1Assets:18AssetManagers:3Local Binders:24Proxy Binders:23Parcel memory:8Parcel count:34Death Recipients:3OpenSSL Sockets:0WebViews:0 SQLMEMORY_USED:345PAGECACHE_OVERFLOW:55MALLOC_SIZE:117 DATABASESpgszdbszLookaside(b)cacheDbname4204117/38/5/data/user/0/com.yuloran.wanandroid_java/databases/app_database.db4120/0/0(attached) temp420403/19/4/data/user/0/com.yuloran.wanandroid_java/databases/app_database.db (1)
-
Private Dirty Memory 類似於之前說過的 Used Memory;
-
Private Clean Memory 類似於 之前說過的 Cached Memory。
下面又介紹了幾種工具,showmap、ahat、debug malloc等,略。。。因為他下面說到:

image
總的來說就是:可以,但沒必要。因為這需要了解很多專業知識,而且很多資料是可見但不可控的。
記憶體優化建議
- 優化 Java 堆上的物件
很多記憶體雖然不在 Java 堆分配,但是其生命週期跟 Java 堆上分配的物件相繫結:

image
所以,優化 Java Heap 上的物件,也有助於其它型別記憶體的回收。
- 減小 apk 體積
因為很多在 apk 中佔據磁碟空間的檔案,在執行期也會佔據記憶體空間:

image
因為 apk 佔據的磁碟空間大小是固定的,所以壓縮 apk 大小比降低記憶體佔用更容易。更多 apk 大小優化方法請檢視 Best Practices to Slim Down Your App Size,視訊地址為:
https://www.youtube.com/watch?v=AdfKNgyT438
02 結論言
本期視訊主要講述了 Android 的 Low Memory Killer 機制、如何評估應用的記憶體使用情況以及如何減少應用記憶體佔用,來源於 Google Android Runtime 開發工程師 Rechard Uhler 的經驗總結,可以說很靠譜了。
就筆者自身的開發經驗來看,記憶體洩露比較容易解決,只是有的洩露是由於第三方 SDK 或者 Framework 導致的,此時只能通過反射來修復。如果反射也修復不了,但是不存在持續洩露,即僅洩露一次,也可以不作處理,或者通過商務推動去解決。而減少記憶體佔用則比較困難,畢竟要想 App 功能豐富,那勢必會佔用更多的記憶體。而且現在很多專案是多人團隊開發,每個人可能只負責一小塊,對整個應用的掌控能力不足,進行記憶體調休就更困難了。所以,記憶體調優工作需要豐富的程式設計經驗及架構經驗,除了 Java 以外,還需要對 Android 的很多 UI 控制元件有比較深入的理解,因為在 Android 平臺上,記憶體佔用大頭永遠是 UI,主要是 Bitmap。
記憶體優化,任重而道遠。
【附】相關架構及資料

image