使用記憶體分析器檢查應用程式的記憶體用量

記憶體分析器是 Android Profiler 中的一個元件,可協助您找出可能導致延遲、沒有回應、甚至應用程式異常終止的記憶體流失情形。這項工具會顯示應用程式記憶體用量的即時圖表,並可讓您擷取記憶體快照資料、強制收集垃圾,以及追蹤記憶體配置。

如要開啟記憶體分析器,請按照下列步驟操作:

  1. 依序按一下「View」>「Tool Windows」>「Profiler」 (您也可以點選工具列中的「分析」圖示 )。
  2. 在 Android 分析器工具列中,選取您要分析的裝置和應用程式程序。如果您已透過 USB 連接裝置,但系統未列出該裝置,請確認您已啟用 USB 偵錯功能
  3. 在「MEMORY」時間軸上的任一處按一下,即可開啟記憶體分析器。

或者,您也可以在指令列中使用 dumpsys 來檢查應用程式記憶體,並查看 logcat 中的 GC 事件

為什麼您必須分析應用程式記憶體

Android 提供受管理記憶體環境;當系統判定應用程式不再使用部分物件時,垃圾收集器就會將未使用的記憶體釋放回堆積中。我們正持續改善 Android 尋找未使用記憶體的運作機制,不過在所有 Android 版本中,系統有時會暫停執行您的程式碼。在多數情況下,這類暫停情形不易察覺。不過,如果應用程式配置記憶體的速度勝過於系統收集記憶體的速度,即使收集器釋出足以滿足配置需求的記憶體,應用程式仍可能會發生延遲情形。這類延遲可能會導致應用程式略過畫格,並讓顯示畫面變慢。

即使應用程式執行速度沒有變慢,但如果發生記憶體流失情形,應用程式在背景中運作時仍會保留該記憶體。這個行為會強制執行不必要的垃圾收集事件,因而降低其餘的系統記憶體效能。系統最後會強制終止應用程式程序來回收記憶體。之後,當使用者返回應用程式時,就必須完全重新啟動。

為避免發生這類問題,建議您使用記憶體分析器執行下列操作:

  • 在時間軸中尋找不理想且可能會導致效能問題的記憶體配置模式。
  • 產生 Java 記憶體快照資料,查看哪些物件正在占用記憶體。您可以在長時間內產生多個記憶體快照資料,用來識別記憶體流失情形。
  • 記錄正常與極端使用者互動情況下的記憶體配置,以明確找出程式碼是否在短時間內配置過多目標物件,或所配置的目標物件發生記憶體流失。

如要進一步瞭解可減少應用程式記憶體用量的程式設計做法,請參閱「管理應用程式的記憶體」。

記憶體分析器總覽

首次開啟記憶體分析器時,畫面會顯示應用程式記憶體用量的詳細時間軸,以及用來強制執行垃圾收集、擷取記憶體快照資料及記錄記憶體配置的存取工具。

圖 1. 記憶體分析器

如圖 1 所示,記憶體分析器的預設檢視畫面包含下列項目:

  1. 強制執行垃圾收集事件的按鈕。
  2. 擷取記憶體快照資料的按鈕。

    注意:只有在連接至搭載 Android 7.1 (API 級別 25 ) 以下版本的裝置時,「記憶體快照資料」按鈕右側才會顯示記錄記憶體配置的按鈕)。

  3. 指定分析器擷取記憶體配置的頻率的下拉式選單。選取適當選項有助於提升分析作業期間的應用程式效能
  4. 縮放時間軸的按鈕。
  5. 跳轉至即時記憶體資料的按鈕。
  6. 顯示活動狀態、使用者輸入內容事件以及螢幕旋轉事件的事件時間軸。
  7. 記憶體用量時間軸,包括下列項目:
    • 堆疊長條圖,顯示每個記憶體類別目前的記憶體用量,以左側 y 軸和頂端的色彩索引鍵表示。
    • 虛線,顯示已配置的物件數量,以右側 y 軸表示。
    • 代表每個垃圾收集事件的圖示。

不過,如果您使用的是搭載 Android 7.1 以下版本的裝置,系統預設不會顯示所有分析資料。如果您看到「Advanced profiling is unavailable for the selected process」的提示訊息,必須啟用進階分析功能才能查看以下內容:

  • 事件時間軸
  • 已配置的物件數量
  • 垃圾收集事件

在 Android 8.0 以上版本中,系統會一律為可偵錯應用程式啟用進階分析功能。

記憶體的計算方式

記憶體分析器頂端顯示的數字 (圖 2) 是根據 Android 系統中應用程式提交的所有專用記憶體頁面。這個總數不包含與系統或其他應用程式共用的頁面。

圖 2. 記憶體分析器頂端的記憶體總數圖例

記憶體總數可分為以下類別:

  • Java:這類記憶體來自透過 Java 或 Kotlin 程式碼配置的物件。
  • Native:這類記憶體來自透過 C 或 C++ 程式碼配置的物件。

    即使您不是使用 C++ 來編寫應用程式,這裡仍可能會顯示一些原生記憶體,這是因為 Android 架構會使用原生記憶體代您處理各項工作,例如處理圖片資源和其他圖像 (即使您使用 Java 或 Kotlin 編寫程式碼)。

  • Graphics:讓圖像緩衝區佇列在螢幕上顯示像素 (包括 GL 表面、GL 材質等) 時所用的記憶體 (請注意,這類記憶體會與 CPU 共用,並不是專屬 GPU 記憶體)。

  • Stack:應用程式中由原生堆疊和 Java 堆疊使用的記憶體。這通常與應用程式執行的執行緒數量有關。

  • Code:應用程式用來處理程式碼和資源 (例如 dex 位元碼、經過最佳化或編譯過的 dex 程式碼、.so 程式庫和字型) 的記憶體。

  • Others:由應用程式使用但系統不確定如何分類的記憶體。

  • Allocated:由應用程式配置的 Java/Kotlin 物件數量。這不會計入 C 或 C++ 中配置的物件。

    如果連接至搭載 Android 7.1 以下版本的裝置,只有在記憶體分析器連線至您執行中的應用程式時,系統才會開始計算記憶體配置量。因此,在您開始進行分析前所配置的任何物件都不會列入計算。然而,Android 8.0 以上版本包含裝置端分析工具,可追蹤所有的記憶體配置量,因此這個數字一律代表應用程式在 Android 8.0 以上版本中執行時產生的 Java 物件總數。

與舊有 Android Monitor 工具的記憶體總數相比,新的記憶體分析器記錄記憶體的方式有所不同,因此您的記憶體用量現在看起來比較高。記憶體分析器會監控一些其他類別的記憶體,因而增加記憶體總數,但如果您只在意 Java 堆積記憶體,則「Java」總數應與舊版工具的值相似。雖然 Java 總數可能與 Android Monitor 中顯示的資料並非完全一致,但從 Zygote 分支以來配置給應用程式 Java 堆積的所有實體記憶體頁面,都會計入這個新總數。因此,這個數字可準確代表應用程式實際使用的實體記憶體總量。

查看記憶體配置

記憶體配置會顯示記憶體中每個 Java 物件和 JNI 參照的配置方式。具體來說,記憶體分析器會顯示下列物件配置相關資訊:

  • 配置的物件類型及其記憶體用量。
  • 每項配置的堆疊追蹤,包括其執行緒所在位置。
  • 物件遭「取消配置」的時間 (僅適用於搭載 Android 8.0 以上版本的裝置)。

如要記錄 Java 和 Kotlin 配置,請選取「Record Java / Kotlin allocations」,再選取「Record」。如果裝置搭載 Android 8 以上版本,記憶體分析器 UI 會轉換至另一個畫面,顯示進行中的記錄情形。您可以與記錄上方的迷你時間軸互動,例如變更選取範圍。如要完成記錄作業,請選取「停止」圖示

記憶體分析器中以視覺化方式呈現 Java 配置

如果裝置搭載 Android 7.1 以下版本,記憶體分析器會使用舊版配置記錄功能,在時間軸上顯示記錄情形,直到您點選「Stop」

選取時間軸的某個區域 (或在搭載 Android 7.1 以下版本的裝置上完成記錄工作階段) 後,系統會顯示已配置的物件清單。這份清單會依類別名稱分組,並依堆積總數排序。

如要檢查配置記錄,請按照下列步驟操作:

  1. 瀏覽清單,找出有異常大量堆積總數且可能流失的物件。為了方便找到已知類別,只要按一下「Class Name」資料欄標題,即可依照字母排序,然後點選類別名稱。視窗右側會出現「Instance View」窗格,顯示該類別的所有執行個體 (如圖 3 所示)。
    • 此外,如要快速找到物件,請按一下「篩選器」圖示 或按下 Control+F 鍵 (在 Mac 上為 Command+F 鍵),並在搜尋欄位中輸入類別或套件名稱。您也可以使用方法名稱搜尋,只要在下拉式選單中選取「Arrange by callstack」即可。如要使用規則運算式,則請勾選「Regex」旁邊的核取方塊。如果您的搜尋查詢有大小寫之分,請勾選「Match case」旁邊的核取方塊。
  2. 在「Instance View」窗格中,點選某個執行個體。下方隨即會出現「Call Stack」分頁標籤,顯示執行個體的配置位置以及位於哪個執行緒中。
  3. 在「Call Stack」分頁標籤中,在任一行上按一下滑鼠右鍵並選取「Jump to Source」,即可在編輯器中開啟對應的程式碼。

圖 3. 右側的「Instance View」會顯示每個已配置物件的詳細資料

已配置物件的清單上方有兩個選單,可用來選擇要檢查的堆積以及資料整理方式。

在左側選單中選擇要檢查的堆積:

  • default heap:當系統未指定堆積時。
  • image heap:系統開機映像檔,包含開機期間已預先載入的類別。此處的配置保證絕不會移動或消失。
  • zygote heap:Android 系統中寫入時複製的堆積,應用程式的程序是從這個堆積分支而來。
  • app heap:主要堆積,應用程式會在這個堆積上配置記憶體。
  • JNI heap:顯示 Java Native Interface (JNI) 參照的配置位置和發布位置的堆積。

在右側選單中選擇配置的安排方式:

  • Arrange by class:根據類別名稱將所有配置分組 (此為預設值)。
  • Arrange by package:根據套件名稱將所有配置分組。
  • Arrange by callstack:將所有配置分成對應的呼叫堆疊群組。

提升分析作業期間的應用程式效能

為了提升分析作業期間的應用程式效能,記憶體分析器預設會定期對記憶體配置進行取樣。在搭載 API 級別 26 以上的裝置上測試時,可使用「Allocation Tracking」 下拉式選單來變更這項行為。可用選項如下:

  • Full:擷取記憶體中的所有物件配置。這是 Android Studio 3.2 以下版本的預設行為。如果您的應用程式會配置大量物件,在分析作業期間可能會發生效能明顯遲緩的情形。
  • Sampled:定期對記憶體中的物件配置進行取樣。這是預設行為,而且在分析作業期間對應用程式效能的影響較小。如果應用程式在短時間內配置大量物件,仍可能會發生效能明顯遲緩的情形。
  • Off:停止追蹤應用程式的記憶體配置。

查看全域 JNI 參照

Java Native Interface (JNI) 是一種允許 Java 程式碼和原生程式碼互相呼叫的架構。

JNI 參照是透過原生程式碼手動管理,因此對於原生程式碼使用的 Java 物件,系統的保留時間可能會過長。如果未先明確刪除就捨棄 JNI 參照,Java 堆積上的部分物件可能會無法存取。此外,您也可能會用盡全域 JNI 參照上限。

如要排解這類問題,請使用記憶體分析器中的「JNI Heap」檢視畫面瀏覽所有的全域 JNI 參照,並依照 Java 類型和原生呼叫堆疊進行篩選。只要透過這項資訊,即可找出全域 JNI 參照的建立位置和刪除位置。

在應用程式執行期間,選取您要檢查的時間軸部分,然後從類別清單上方的下拉式選單中選取「JNI heap」。接著,您可以照常檢查堆積中的物件,並在「Allocation Call Stack」分頁標籤中按兩下物件,查看 JNI 參照在程式碼中的配置和發布位置 (如圖 4 所示)。

圖 4. 查看全域 JNI 參照

如要檢查應用程式 JNI 程式碼的記憶體配置,必須將應用程式部署至搭載 Android 8.0 以上版本的裝置。

如要進一步瞭解 JNI,請參閱「JNI 提示」。

原生記憶體分析器

Android Studio 記憶體分析器包含原生記憶體分析器,適用於部署至 Android 10 實體裝置的應用程式;Android Studio 4.2 預覽版本目前支援 Android 11 的裝置。

原生記憶體分析器會追蹤特定時間範圍內原生程式碼中的物件配置和取消配置情形,並提供下列資訊:

  • Allocations:在所選時間範圍內,透過 malloc()new 運算子配置的物件數量。
  • Deallocations:在所選時間範圍內,透過 free()delete 運算子取消配置的物件數量。
  • Allocations Size:在所選時間範圍內,所有配置的匯總大小 (以位元組為單位)。
  • Deallocations Size:在所選時間範圍內,所有已釋放記憶體的匯總大小 (以位元組為單位)。
  • Total Count:「Allocations」資料欄中的值減去「Deallocations」資料欄中的值所得的結果。
  • Remaining Size:「Allocations size」資料欄中的值減去「Deallocations size」資料欄中的值所得的結果。

原生記憶體分析器

如要針對搭載 Android 10 以上版本的裝置記錄原生配置,請依序選取「Record native allocations」>「Record」。系統會持續記錄到您點選「停止」圖示 為止,在那之後,記憶體分析器 UI 會轉換至另一個顯示原生記錄的畫面。

記錄原生配置按鈕

如果是搭載 Android 9 以下版本的裝置,則無法使用「Record native allocations」選項。

根據預設,原生記憶體分析器使用的樣本大小為 32 位元組;意即每配置 32 個位元組的記憶體時,系統就會建立記憶體快照。樣本大小越小,快照擷取頻率就越高,進而產生更準確的記憶體用量資料。樣本大小越大,資料準確度就越低,但消耗的系統資源較少,並且能提升分析作業期間的效能。

如何變更原生記憶體分析器的樣本大小:

  1. 依序選取「Run」>「Edit Configurations」
  2. 在左側窗格中選取您的應用程式模組。
  3. 按一下「Profiling」分頁標籤,然後在標示為「Native memory sampling interval (bytes)」的欄位中輸入樣本大小。
  4. 再次建構並執行應用程式。

擷取記憶體快照資料

記憶體快照資料會顯示在您擷取記憶體快照資料時,應用程式中有哪些物件正在使用記憶體。尤其是在長時間使用者工作階段之後,記憶體快照資料可顯示您認為不應位於記憶體中、但仍位於記憶體中的物件,藉此找出記憶體流失情形。

擷取記憶體快照資料後,您可以查看下列資料:

  • 應用程式已配置的物件類型及各類數量。
  • 每個物件的記憶體用量。
  • 在程式碼中保留物件參照的位置。
  • 物件配置位置的呼叫堆疊 (目前,在記錄配置期間,只有透過 Android 7.1 以下版本擷取記憶體快照資料時,才能使用呼叫堆疊)。

如要擷取記憶體快照資料,請按一下「Capture heap dump」,然後選取「Record」。擷取記憶體快照資料時,Java 記憶體的數量可能會暫時增加。這是正常現象,因為記憶體快照資料會在與應用程式相同的程序中發生,因此需要一些記憶體來收集資料。

分析器完成擷取記憶體快照資料後,記憶體分析器 UI 會轉換至另一個顯示記憶體快照資料的畫面。

圖 5. 查看記憶體快照資料。

如要仔細瞭解快照資料的建立時間,您可以呼叫 dumpHprofData() 在應用程式程式碼的重要位置上建立記憶體快照資料。

您可以在類別清單中查看下列資訊:

  • Allocations:堆積中的配置數量。
  • Native Size:該物件類型使用的原生記憶體總量 (以位元組為單位)。只有 Android 7.0 以上版本才會顯示這個資料欄。

    這裡會顯示 Java 中已配置的部分物件,因為 Android 會針對某些架構類別 (例如 Bitmap) 使用原生記憶體。

  • Shallow Size:該物件類型使用的 Java 記憶體總量 (以位元組為單位)。

  • Retained Size:該類別的所有執行個體所保留的記憶體總大小 (以位元組為單位)。

已配置物件的清單上方有兩個選單,可用來選擇要檢查的記憶體快照資料以及資料整理方式。

在左側選單中選擇要檢查的堆積:

  • default heap:當系統未指定堆積時。
  • app heap:主要堆積,應用程式會在這個堆積上配置記憶體。
  • image heap:系統開機映像檔,包含開機期間已預先載入的類別。此處的配置保證絕不會移動或消失。
  • zygote heap:Android 系統中寫入時複製的堆積,應用程式的程序是從這個堆積分支而來。

在右側選單中選擇配置的安排方式:

  • Arrange by class:根據類別名稱將所有配置分組 (此為預設值)。
  • Arrange by package:根據套件名稱將所有配置分組。
  • Arrange by callstack:將所有配置分成對應的呼叫堆疊群組。只有在記錄配置期間的記憶體快照資料時,才能使用這個選項。即使如此,堆積中也可能有在您開始記錄前已配置的物件,因此系統會優先顯示這些配置 (依類別名稱列出)。

系統預設會依照「Retained Size」資料欄排序這份清單。如要以其他資料欄中的值排序,請點選該資料欄的標題。

按一下類別名稱即可開啟右側的「Instance View」視窗 (如圖 6 所示)。所列的每種執行個體都包含下列項目:

  • Depth:從任何 GC 根目錄到所選執行個體的最短躍點。
  • Native Size:原生記憶體中該執行個體的大小。只有 Android 7.0 以上版本才會顯示這個資料欄。
  • Shallow Size:Java 記憶體中該執行個體的大小。
  • Retained Size:該執行個體支配的記憶體大小 (請參閱「支配樹狀圖」)。

圖 6. 時間軸中會顯示擷取記憶體快照資料所需時間

如要檢查堆積,請按照以下步驟操作:

  1. 瀏覽清單,找出有異常大量堆積總數且可能流失的物件。為了方便找到已知類別,只要按一下「Class Name」資料欄標題,即可依照字母排序,然後點選類別名稱。視窗右側會出現「Instance View」窗格,顯示該類別的所有執行個體 (如圖 6 所示)。
    • 此外,如要快速找到物件,請按一下「篩選器」圖示 或按下 Control+F 鍵 (在 Mac 上為 Command+F 鍵),並在搜尋欄位中輸入類別或套件名稱。您也可以使用方法名稱搜尋,只要在下拉式選單中選取「Arrange by callstack」即可。如要使用規則運算式,則請勾選「Regex」旁邊的核取方塊。如果您的搜尋查詢有大小寫之分,請勾選「Match case」旁邊的核取方塊。
  2. 在「Instance View」窗格中,點選某個執行個體。「References」分頁標籤下方會顯示該物件的所有參照。

    或者,按一下執行個體名稱旁邊的箭頭即可查看所有欄位,接著按一下欄位名稱即可查看所有參照。如要查看某個欄位的執行個體詳細資料,請在欄位上按一下滑鼠右鍵並選取「Go to Instance」

  3. 在「References」分頁中,如果您發現某個參照可能會導致記憶體流失,請在該參照上按一下滑鼠右鍵,並選取「Go to Instance」。這項操作會從記憶體快照資料中選取對應的執行個體,並顯示該快照的執行個體資料。

在記憶體快照資料中,請注意下列任一原因所導致的記憶體流失情形:

  • 長時間參照 ActivityContextViewDrawable 以及可能會保留 ActivityContext 容器參照的物件。
  • 可保留 Activity 執行個體的非靜態內部類別 (例如 Runnable)。
  • 保留物件超過必要時間的快取。

將記憶體快照資料儲存為 HPROF 檔案

擷取記憶體快照資料後,只有在記憶體分析器執行期間才能查看分析器中的資料。一旦結束分析工作階段,就會遺失記憶體快照資料。因此,如果您要儲存資料以供日後查看,可將記憶體快照資料匯出成 HPROF 檔案。在 Android Studio 3.1 以下版本中,時間軸下方的工具列左側會顯示「將擷取內容匯出成檔案」按鈕 ;而在 Android Studio 3.2 以上版本中,「Sessions」窗格內的每個「記憶體快照資料」項目右側都會有「匯出記憶體快照資料」按鈕。在隨即顯示的「Export As」對話方塊中,使用 .hprof 副檔名儲存檔案。

如要使用其他 HPROF 分析工具 (例如 jhat),您必須將 HPROF 檔案從 Android 格式轉換為 Java SE HPROF 格式。您可以使用 android_sdk/platform-tools/ 目錄中的 hprof-conv 工具完成這項操作。接著執行含有兩個引數 (原始 HPROF 檔案和已轉換 HPROF 檔案寫入位置) 的 hprof-conv 指令,範例如下:

hprof-conv heap-original.hprof heap-converted.hprof

匯入記憶體快照資料檔案

如要匯入 HPROF (.hprof) 檔案,請按一下「Sessions」窗格中的「啟動新的分析工作階段」圖示 ,選取「Load from file」,然後從檔案瀏覽器中選擇檔案。

您也可以將 HPROF 檔案從檔案瀏覽器拖曳至編輯器視窗中,藉此匯入檔案。

記憶體分析器中的記憶體流失偵測功能

在記憶體分析器中分析記憶體快照資料時,可篩選 Android Studio 認為可能代表應用程式中的 ActivityFragment 執行個體導致記憶體流失的分析資料。

這項篩選條件會顯示的資料類型包括:

  • 已遭刪除但仍受參照的 Activity 執行個體。
  • 不含有效 FragmentManager 但仍受參照的 Fragment 執行個體。

在某些情況下,這項篩選條件可能會產生誤判結果,例如:

  • Fragment 已完成建立但未曾使用。
  • 系統正在為 Fragment 建立快取,但這項作業不屬於 FragmentTransaction 的一部分。

如要使用這項功能,請先擷取記憶體快照資料,或將記憶體快照資料檔案匯入 Android Studio。如要顯示可能導致記憶體流失的片段和活動,請在記憶體分析器的記憶體快照資料窗格中,勾選「Activity/Fragment Leaks」核取方塊 (如圖 7 所示)。

分析器:記憶體流失情形偵測功能

圖 7. 篩選記憶體快照資料,掌握記憶體流失情形。

分析記憶體的技巧

使用記憶體分析器時,請對應用程式程式碼施加壓力,並嘗試強制發生記憶體流失問題。如要在應用程式中引發記憶體流失,其中一個方法就是在檢查堆積前先讓應用程式運作一段時間。記憶體流失情形可能會逐漸蔓延至堆積中的配置頂部。不過,流失情形越少,就需要更長的應用程式執行時間,以便觀察流失情形。

您也可以透過下列任一方式觸發記憶體流失:

  • 在不同的活動狀態下,將裝置從直向轉為橫向再轉回直向,依此多次反覆執行。旋轉裝置經常會導致應用程式的 ActivityContextView 物件流失,這是因為系統會重新建立 Activity,而且如果應用程式在其他位置保留其中任一物件的參照,系統將無法執行垃圾收集。
  • 在不同的活動狀態下,在您的應用程式與其他應用程式之間切換 (前往主畫面,然後返回應用程式)。

提示:您也可以使用 monkeyRunner 測試架構來執行上述步驟。