分析並優化 Android 應用記憶體佔用

Snake_sss發表於2018-12-30

官方視訊翻譯,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

演講人介紹

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

分析並優化 Android 應用記憶體佔用

視訊地址

前言

想要進行記憶體優化,就必須對 Android 記憶體管理機制有比較深入的瞭解,這樣才能保證應用在低端機上也能有良好的表現。不同的記憶體型別,包括 Shared Memory,Dex Memory 以及 GPU Memory, 都會對使用者體驗產生影響。

我在過去的三年時間裡,都在致力於深入理解 Android 應用記憶體管理機制。那麼,為什麼 App 開發工程師也要關注記憶體佔用呢?於我而言,主要是因為 Android 生態系統。如果一個 Android 應用在低端裝置上使用者體驗不好(比如經常卡頓),那麼 OEM(Original Entrusted Manufacture) 就不願再生產這樣的裝置,進而導致這部分使用者被排除在 Android 生態系統之外。

本次課題主要討論三點內容:

  • 低記憶體時 Android 系統的工作機制

  • 如何評估應用記憶體使用情況

  • 如何減少應用記憶體佔用

低記憶體時 Android 系統的工作機制

首先,需要介紹實體記憶體的概念,然後引入 Android Low Memory Killer。

實體記憶體

裝置的實體記憶體被分為很多頁(Page),每頁 4KB。不同的頁用來做不同的事情:

分析並優化 Android 應用記憶體佔用

橘黃色的是已使用頁,黃色的是快取頁(資料在磁碟上有備份,所以 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 太少的話,裝置就可能當機:

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

如上圖所示,當 Cached Pages 低於 LMK 閾值時,將會觸發低記憶體殺當機制。

LMK(Low Memory Killer)

如果 LMK 殺掉的是使用者正在互動或可以感知的程式,將會導致非常不友好的使用者體驗。所以 Android SystemServer 程式維護了一張程式優先順序列表,LMK 根據這張表來決定先殺死哪個程式:

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

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

所以,你可以想象 LMK 在低記憶體手機上的情景:

分析並優化 Android 應用記憶體佔用

如上圖所示,LMK 將一直處於活躍狀態,具體表現就是應用卡頓、桌面黑屏重啟,手機當機等等。如此,OEM 將不願生產這些裝置。

評估應用記憶體使用情況

那麼,我們怎麼知道 App 使用了多少記憶體呢?

實體記憶體追蹤

之前提到,裝置的實體記憶體被分為很多頁(Page),Linux Kernel 將會持續跟蹤每個程式使用的 Pages,所以只要對程式使用的 Pages 進行計數即可:

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

共享記憶體頁計數方法:

(1)RSS(Resident Set Size):App 完全負責:

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

但實際上,至少需要系統級別的上下文才能知道識別 RSS 與 USS。所以通常都是使用 PSS 來計算,這也可以避免多計或者少計 Shared Pages。你可以使用:

adb shell dumpsys meminfo -s [process] 
複製程式碼

命令來檢視一個程式的 PSS 使用情況:

分析並優化 Android 應用記憶體佔用

最底部的 TOTAL 代表的就是應用按比例佔用的總記憶體大小。

應用記憶體佔用分析

如果想要應用支援的功能越多,UI 越炫酷,那就需要更多的記憶體分配。既想馬兒跑,又想馬兒不吃草的事情是不存在的:

分析並優化 Android 應用記憶體佔用

記憶體佔用影響因素:

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

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

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

分析並優化 Android 應用記憶體佔用

由於記憶體壓力不好控制,所以建議評估前,先一鍵清理所有程式,然後再測試。

減少應用記憶體佔用

使用 Android Studio 的 Memory Profiler,可以檢視當前 Java 堆上分配了哪些物件、物件大小以及物件引用鏈和被引用鏈等很多資訊。Live Allocation 中有 image heap、zygote heap、app heap 等可以選擇,但是我建議你只關注 app heap。因為 image heap 和 zygote heap 是 App 啟動時從系統繼承過來的,對於這部分記憶體佔用,我們基本上無能為力:

分析並優化 Android 應用記憶體佔用

關於 Memory Profiler 的細節我不會講太多,因為明天中午 12:30 Esteban 將會詳細講解 Profiler 的用法,畢竟這是他們團隊開發的。所以,我強力推薦你們也參加一下明天的宣講會。

Java Heap 以外的記憶體佔用分析

上面提到,TOTAL 是 PSS,那麼這張圖中,除了 Java Heap,其它的是什麼意思呢?對於這部分記憶體佔用,我們又能做什麼呢?

分析並優化 Android 應用記憶體佔用

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

順便說一下,我生活在這兒,這個橘黃色的方塊裡(Android Runtime):

分析並優化 Android 應用記憶體佔用

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] **
                   Pss      Pss   Shared  Private   Shared  Private  SwapPss     Heap     Heap     Heap
                 Total    Clean    Dirty    Dirty    Clean    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap    35822        0      824    35764       32       24     8740    75776    38786    36989
  Dalvik Heap     4001        0      304     3552       72      412      240     6847     3424     3423
 Dalvik Other     5256        0       48     5256        0        0        0                           
        Stack      120        0        4      120        0        0        0                           
       Ashmem      130        0        4      128        4        0        0                           
      Gfx dev     2596        0        0     2596        0        0        0                           
    Other dev       16        0      104        0        0       16        0                           
     .so mmap    23782    22188     1132      504    13320    22188       15                           
    .jar mmap       68        0        8       68        0        0        0                           
    .apk mmap     8029       24        0     7684     1872       24        0                           
    .ttf mmap      223       20        0        0      956       20        0                           
    .dex mmap    21974    19864        0       20    13080    19864        0                           
    .oat mmap      377       64        0        0     3620       64        0                           
    .art mmap     6547      404      868     5852     7584      404       24                           
   Other mmap      408        0       12        8      644      376        0                           
   EGL mtrack    24660        0        0    24660        0        0        0                           
    GL mtrack     4524        0        0     4524        0        0        0                           
      Unknown     2130        0      184     2124        0        0        0                           
        TOTAL   140702    42564     3492    92860    41184    43392       39    82623    42210    40412
 
 Dalvik Details
        .Heap     3308        0        0     3308        0        0        0                           
         .LOS       42        0       16       12        4       28        4                           
 .LinearAlloc     4020        0       20     4020        0        0        0                           
          .GC      384        0       16      384        0        0        0                           
    .JITCache      596        0        0      596        0        0        0                           
      .Zygote      583        0      288      164       68      384        0                           
   .NonMoving       68        0        0       68        0        0        0                           
 .IndirectRef      256        0       12      256        0        0        0                           
 
 App Summary
                       Pss(KB)
                        ------
           Java Heap:     9808
         Native Heap:    35764
                Code:    50436
               Stack:      120
            Graphics:    31780
       Private Other:     8344
              System:     4450
 
               TOTAL:   140702       TOTAL SWAP PSS:       39
 
 Objects
               Views:      207         ViewRootImpl:        1
         AppContexts:        3           Activities:        1
              Assets:       18        AssetManagers:        3
       Local Binders:       24        Proxy Binders:       23
       Parcel memory:        8         Parcel count:       34
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
 
 SQL
         MEMORY_USED:      345
  PAGECACHE_OVERFLOW:       55          MALLOC_SIZE:      117
 
 DATABASES
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4       20             41        17/38/5  /data/user/0/com.yuloran.wanandroid_java/databases/app_database.db
         4       12                         0/0/0    (attached) temp
         4       20             40         3/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等,略。。。因為他下面說到:

分析並優化 Android 應用記憶體佔用

總的來說就是:可以,但沒必要。因為這需要了解很多專業知識,而且很多資料是可見但不可控的。

記憶體優化建議

(1)優化 Java 堆上的物件:

很多記憶體雖然不在 Java 堆分配,但是其生命週期跟 Java 堆上分配的物件相繫結:

分析並優化 Android 應用記憶體佔用

所以,優化 Java Heap 上的物件,也有助於其它型別記憶體的回收。

(2)減小 apk 體積:

因為很多在 apk 中佔據磁碟空間的檔案,在執行期也會佔據記憶體空間:

分析並優化 Android 應用記憶體佔用

因為 apk 佔據的磁碟空間大小是固定的,所以壓縮 apk 大小比降低記憶體佔用更容易。更多 apk 大小優化方法請檢視 Best Practices to Slim Down Your App Size

結語

本期視訊主要講述了 Android 的 Low Memory Killer 機制、如何評估應用的記憶體使用情況以及如何減少應用記憶體佔用,來源於 Google Android Runtime 開發工程師 Rechard Uhler 的經驗總結,可以說很靠譜了。

就筆者自身的開發經驗來看,記憶體洩露比較容易解決,只是有的洩露是由於第三方 SDK 或者 Framework 導致的,此時只能通過反射來修復。如果反射也修復不了,但是不存在持續洩露,即僅洩露一次,也可以不作處理,或者通過商務推動去解決。而減少記憶體佔用則比較困難,畢竟要想 App 功能豐富,那勢必會佔用更多的記憶體。而且現在很多專案是多人團隊開發,每個人可能只負責一小塊,對整個應用的掌控能力不足,進行記憶體調休就更困難了。所以,記憶體調優工作需要豐富的程式設計經驗及架構經驗,除了 Java 以外,還需要對 Android 的很多 UI 控制元件有比較深入的理解,因為在 Android 平臺上,記憶體佔用大頭永遠是 UI,主要是 Bitmap。

記憶體優化,任重而道遠。

相關文章