使用 Android Studio Profiler 工具解析應用的記憶體和 CPU 使用資料
為了幫助開發者開發出更加輕快高效的應用,我們在 Android Studio 3.0 以及更高版本中加入了 Android Profiler 工具,用於應用的 CPU、記憶體、網路和能耗分析。
Memory Profiler
許多開發者使用 Memory Profiler,是希望發現和定位記憶體洩漏問題。在介紹 Memory Profile 如何解決這一問題之前,我想先明確 "記憶體洩漏" 這一概念。無論您當前是否瞭解記憶體洩漏,都將幫助我更好地解釋 Memory Profile 的工作原理。
記憶體洩漏
什麼是記憶體洩漏?
通常我們認為,在執行的程式中,如果一個無法訪問的物件卻仍然佔用著記憶體空間,即為此物件造成了記憶體洩漏。如果您使用過 C 語言或 C++ 的指標,您會很熟悉這個概念。
但是在 Kotlin 和 Java 的世界中,事情有些許不同。因為這兩種語言是執行在 Java 虛擬機器器 (JVM) 中的。在 JVM 中,有個重要的概念,就是垃圾回收 (GC)。當垃圾回收執行時,虛擬機器器會首先識別 GC Root。GC Root 是一個可以從堆外部訪問的物件,它可以是本地變數或執行中的執行緒等。虛擬機器器會識別所有可以從 GC Root 訪問的物件,它們將會被保留。而其他無法從 GC root 訪問的物件,則會被認為是垃圾並回收掉。
Activity 和 Fragment 洩漏檢測
在 Android 應用中,應當尤為警惕 Activity 和 Fragment 物件的洩漏,因為這兩種物件通常都會佔用很多記憶體。在 Android 3.6 中,Memory Profiler 加入了自動檢查 Activity 和 Fragment 中的記憶體洩漏的功能。使用這一功能非常的簡單:
- 首先,您需要在 Memory Profiler 中儲存 Heap Dump,點選下圖所示按鈕:
- 在 Heap Dump 載入完成後,勾選 "Activity/Fragment Leaks" 選框:
Memory Profiler 通過以下幾種場景來判斷洩漏是否發生:
- 當我們銷燬了一個 Activity 的例項後,這個例項就再也不會被使用了。此時如果仍然有這個 Activity 的引用,Memory Profiler 就會認為它已經洩漏;
- Fragment 的例項應當與一個 Fragment Manager 相關聯,如果我們看到一個 Fragment 沒有關聯任何一個 Fragment Manager,而且它依然被引用時,也可以認為有洩漏發生。
不過要注意的是,針對 Fragment 有個特別的情況: 如果您載入的 Heap Dump 的時機,剛好介於 Fragment 被建立和被使用的時間之間,就會造成 Memory Profiler 誤報;相同情況也會發生在 Fragment 被快取但是沒有被複用的時候。
其他記憶體洩漏檢測
Memory Profiler 也可以用於檢查其他型別的洩漏,它提供了許多資訊,用於幫助您識別記憶體洩漏是否發生。
當您拿到一段 Heap Dump 之後,Memory Profiler 會展示出類的列表。對於每個類,"Allocation" 這一列顯示的是它的例項數量。而在它右邊則依次是 "Native Size"、"Shallow Size" 和 "Retained Size":
我們用下圖來表示某段 Heap Dump 記錄的應用記憶體狀態。注意紅色的節點,在這個示例中,這個節點所代表的物件從我們的工程中引用了 Native 物件:
先從 "Shallow Size" 講起,這列資料其實非常簡單,就是物件本身消耗的記憶體大小,在上圖中,即為紅色節點自身所佔記憶體。
而 "Native Size" 同樣也很簡單,它是類物件所引用的 Native 物件 (藍色節點) 所消耗的記憶體大小:
由於一旦刪除紅色節點,其餘的橙色節點都將無法被訪問,這時候它們就會被 GC 回收掉。從這個角度上講,它們是被紅色節點所持有的,因此被命名為 "Retained Size"。
還有一個前面沒有提到的資料維度。當您點選某個類名,介面中會顯示這個類例項列表,這裡有一列新資料 —— "Depth":
以紅色節點為例,如果從其左邊來的任何一個引用被破壞,紅色節點就會變成不可訪問的狀態並且被垃圾回收回收掉。而對於右邊的藍色節點來說,如果您希望它被垃圾回收,那您需要把左右兩邊的路徑都破壞才行。
值得警惕的是,如果您看到某個例項的 "Depth" 為 1 的話,這意味著它直接被 GC root 引用,同時也意味著它永遠不會被自動回收。
下面是一個示例 Activity,它實現了 LocationListener 介面,高亮部分程式碼 "requestLocationUpdates" 將會使用當前 Activity 例項來註冊 locationManager。如果您忘記登出,這個 Activity 就會洩漏。它將永遠都待在記憶體裡,因為位置管理器是一個 GC root,而且永遠都存在:
CPU Profiler
和 Memory Profiler 類似,CPU Profiler 提供了從另一個角度記錄和分析應用關鍵效能資料的方法。
使用 CPU Profiler,首先要產生一些 CPU 的使用記錄:
- 進入 Android Studio 中的 CPU Profiler 介面,在您的應用已經部署的前提下,點選 "Record" 按鈕;
- 在應用中進行您想要分析的操作;
- 返回 CPU Profiler,點選 "Stop" 按鈕。
由於最終呈現的資料是基於執行緒組織的,所以去觀察資料之前,您應該確認是否選擇了正確的執行緒:
Call Chart
在 CPU Profiler 介面下半部,有四個標籤頁,分別對應四個不同的資料圖表,它們分別是: Call Chart、Flame Chart、Top Down 和 Bottom Up。其中的 Call Chart 可能是最直白的一個,它基本上就是一個呼叫棧的重新組織和視覺化呈現:
Flame Chart
Flame Chart 提供了一個呼叫棧的聚合資訊。與 Call Chart 不同的是,它的橫軸顯示的是百分比數值。由於忽略了時間線資訊,Flame Chart 可以展示每次呼叫消耗時間佔用整個記錄時長的百分比。同時縱軸也被對調了,在頂部展示的是被呼叫者,底部展示的是呼叫者。此時的圖表看起來越往上越窄,就好像火焰一樣,因此得名:
前面介紹的兩種圖表,可以幫助我們從兩種角度縱覽全域性。而如果我們需要更精確的時間資訊,就需要使用 Top Down Tree。在 CPU Profiler 中,Top Down 選項卡展示的是一個資料表格,為了便於理解其中各組資料的意義,接下來我們會嘗試構建一個 Top Down Tree。
構建一個 Top Down Tree 並不複雜。以 Flame Chart 為基礎,您只需要從呼叫者開始,持續新增被呼叫者作為子節點,直到整個 Flame Chart 被遍歷一遍,您就獲得了一個 Top Down Tree:
- Self Time —— 執行自己的程式碼所消耗的時間;
- Children Time —— 呼叫其他方法的時間;
- Total Time —— 前面兩者時間之和。
有了 Top Down Tree,我們能輕易將這三組資訊歸納到一個表格之中:
我們從 A 節點開始:
- A 消耗了 1 秒鐘來執行自己的程式碼,所以 Self Time 是 1;
- 然後它消耗了 9 秒中去呼叫其他方法,這意味著它的 Children Time 是 9;
- 這樣就一共消耗了 10 秒鐘,Total Time 是 10;
- B 和 D 以此類推...
值得注意的是,D 節點只是呼叫了 C,自己沒做任何事,這種情況在方法封裝時很常見。所以 D 的 Children Time 和 Total Time 都是 2。
下面是表格完全展開的狀態。當您在 Android Studio 中分析應用時,CPU Profiler 會完成上面所有的計算,您只要理解這些數字是怎麼產生的即可:
Bottom Up Tree
當您希望方便地找到某個方法的呼叫棧時,Bottom Up Tree 就派上用場了。"樹" 如其名,Bottom Up Tree 從底部開始構建,這樣我們就能通過在節點上不斷新增呼叫者來反向構建出樹。由於每個獨立節點都可以構建出一棵樹,所以這裡其實是森林 (Forest):
表格有四行,因為我們有四個樹在森林中。從節點 C 開始:
- Self Time 是 4 + 2 = 6 秒鐘;
- C 沒有呼叫其他方法,所以 Children Time 是 0;
- 前面兩者相加,總時間為 6 秒鐘。
看起來與 Top Bottom Tree 別無二致。接下來展開 C 節點,計算 C 的呼叫者 B 和 D 的情況。
在計算 B 和 D 節點的相關時間時,情況與前面的 Top Bottom Tree 有所不同:
- 由於我們在構建基於 C 節點的 Bottom Up Tree,所以所有時間資訊也都是基於 C 節點的。這時我們在計算 B 的 Self Time 時,應當計算 C 被 B 呼叫的時間,而不是 B 自身執行的時間,這裡是 4 秒;對於 D 來說,則是 2 秒。
- 由於只有 B 和 D 呼叫 C 的方法,它們的 Total Time 之和應與 C 的 Total Time 相等。
下一個樹是 B 節點的 Bottom Up Tree,它的 Self Time 是 3 秒,Children Time 是用來呼叫其他方法的時間,這裡只有 C,所以是 2 秒。Total Time 永遠都是前兩者之和。下面便是整個表格展開的樣子:
備忘表
前面介紹了四種不同的資料圖表,並且還詳細解釋了一些資料是如何被計算出來的。如果您覺得頭緒太多很難記住,沒關係,下面這個簡明的備忘表就是為您準備的:
總結
本文介紹了 Android Studio Profiler 中的兩種資料分析工具。
其中 Memory Profiler 可以自動檢測 Activity 和 Fragment 的記憶體洩漏,而通過瞭解和使用 Memory Profiler 中資料分析功能提供的資料,也可以發現和解決其他型別的記憶體洩漏問題。
有關 CPU Profiler 則介紹了 Call Chart、Flame Chart、Top Down、Bottom Up 這四種維度的資料呈現。
希望這些內容能夠幫助您更加了解 Android Profiler。如仍有疑問,歡迎在下方留言。也歡迎通過 Android Studio 反饋使用中遇到的問題。
您也可以通過視訊回顧 2019 Android 開發者峰會演講 —— 讀懂 Android Studio 分析工具資料:
點選這裡即刻閱讀更多應用效能相關內容
