使用 Android Studio Profiler 工具解析應(yīng)用的內(nèi)存和 CPU 使用數(shù)據(jù)

為了幫助開發(fā)者開發(fā)出更加輕快高效的應(yīng)用,我們在 Android Studio 3.0 以及更高版本中加入了 Android Profiler 工具,用于應(yīng)用的 CPU、內(nèi)存、網(wǎng)絡(luò)和能耗分析。

在 Android Profiler 提供的這四種性能數(shù)據(jù)中,絕大多數(shù)場景下我們都更關(guān)心 CPU 和內(nèi)存的使用情況。本文將介紹對應(yīng)的兩種分析工具 —— Memory Profiler 和 CPU Profiler。

Memory Profiler

許多開發(fā)者使用 Memory Profiler,是希望發(fā)現(xiàn)和定位內(nèi)存泄漏問題。在介紹 Memory Profile 如何解決這一問題之前,我想先明確 "內(nèi)存泄漏" 這一概念。無論您當(dāng)前是否了解內(nèi)存泄漏,都將幫助我更好地解釋 Memory Profile 的工作原理。

內(nèi)存泄漏

什么是內(nèi)存泄漏?

通常我們認(rèn)為,在運行的程序中,如果一個無法訪問的對象卻仍然占用著內(nèi)存空間,即為此對象造成了內(nèi)存泄漏。如果您使用過 C 語言或 C++ 的指針,您會很熟悉這個概念。

但是在 Kotlin 和 Java 的世界中,事情有些許不同。因為這兩種語言是運行在 Java 虛擬機(jī) (JVM) 中的。在 JVM 中,有個重要的概念,就是垃圾回收 (GC)。當(dāng)垃圾回收運行時,虛擬機(jī)會首先識別 GC Root。GC Root 是一個可以從堆外部訪問的對象,它可以是本地變量或運行中的線程等。虛擬機(jī)會識別所有可以從 GC Root 訪問的對象,它們將會被保留。而其他無法從 GC root 訪問的對象,則會被認(rèn)為是垃圾并回收掉。

所以,一般意義上的內(nèi)存泄漏在 JVM 中并不存在。在 JVM 中的內(nèi)存泄漏通常是指: 內(nèi)存中含有那些再也不會被使用、但是仍然能夠訪問的對象。

Activity 和 Fragment 泄漏檢測

在 Android 應(yīng)用中,應(yīng)當(dāng)尤為警惕 Activity 和 Fragment 對象的泄漏,因為這兩種對象通常都會占用很多內(nèi)存。在 Android 3.6 中,Memory Profiler 加入了自動檢查 Activity 和 Fragment 中的內(nèi)存泄漏的功能。使用這一功能非常的簡單:

首先,您需要在 Memory Profiler 中保存 Heap Dump,點擊下圖所示按鈕:

在 Heap Dump 加載完成后,勾選 "Activity/Fragment Leaks"?選框:

此時如果有檢查到 Activity 或 Fragment 的泄漏,就會在界面中顯示出來。

Memory Profiler 通過以下幾種場景來判斷泄漏是否發(fā)生:

? ? 當(dāng)我們銷毀了一個 Activity 的實例后,這個實例就再也不會被使用了。此時如果仍然有這個 Activity 的引用,Memory Profiler 就會認(rèn)為它已經(jīng)泄漏;

? ? Fragment 的實例應(yīng)當(dāng)與一個 Fragment Manager 相關(guān)聯(lián),如果我們看到一個 Fragment 沒有關(guān)聯(lián)任何一個 Fragment Manager,而且它依然被引用時,也可以認(rèn)為有泄漏發(fā)生。

不過要注意的是,針對 Fragment 有個特別的情況:?如果您載入的 Heap Dump 的時機(jī),剛好介于 Fragment 被創(chuàng)建和被使用的時間之間,就會造成 Memory Profiler 誤報;相同情況也會發(fā)生在 Fragment 被緩存但是沒有被復(fù)用的時候。

其他內(nèi)存泄漏檢測

Memory Profiler 也可以用于檢查其他類型的泄漏,它提供了許多信息,用于幫助您識別內(nèi)存泄漏是否發(fā)生。

當(dāng)您拿到一段 Heap Dump 之后,Memory Profiler 會展示出類的列表。對于每個類,"Allocation"?這一列顯示的是它的實例數(shù)量。而在它右邊則依次是 "Native Size"、"Shallow Size"?和 "Retained Size":

這幾組數(shù)據(jù)分別意味著什么呢?下面我會通過一個例子來說明。

我們用下圖來表示某段 Heap Dump 記錄的應(yīng)用內(nèi)存狀態(tài)。注意紅色的節(jié)點,在這個示例中,這個節(jié)點所代表的對象從我們的工程中引用了 Native 對象:

這種情況不太常見,但在 Android 8.0 之后,使用 Bitmap 便可能產(chǎn)生此類情景,因為 Bitmap 會把像素信息存儲在原生內(nèi)存中來減少 JVM 的內(nèi)存壓力。

先從 "Shallow Size" 講起,這列數(shù)據(jù)其實非常簡單,就是對象本身消耗的內(nèi)存大小,在上圖中,即為紅色節(jié)點自身所占內(nèi)存。

而 "Native Size"?同樣也很簡單,它是類對象所引用的 Native 對象 (藍(lán)色節(jié)點) 所消耗的內(nèi)存大小:

"Retained Size" 稍復(fù)雜些,它是下圖中所有橙色節(jié)點的大小:

由于一旦刪除紅色節(jié)點,其余的橙色節(jié)點都將無法被訪問,這時候它們就會被 GC 回收掉。從這個角度上講,它們是被紅色節(jié)點所持有的,因此被命名為 "Retained Size"。

還有一個前面沒有提到的數(shù)據(jù)維度。當(dāng)您點擊某個類名,界面中會顯示這個類實例列表,這里有一列新數(shù)據(jù) —— "Depth":

"Depth"?是從 GC Root 到達(dá)這個實例的最短路徑,圖中的這些數(shù)字就是每個對象的深度 (Depth):

一個對象離 GC Root 越近,它就越有可能與 GC Root 有多條路徑相連,也就越可能在垃圾回收中被保存下來。

以紅色節(jié)點為例,如果從其左邊來的任何一個引用被破壞,紅色節(jié)點就會變成不可訪問的狀態(tài)并且被垃圾回收回收掉。而對于右邊的藍(lán)色節(jié)點來說,如果您希望它被垃圾回收,那您需要把左右兩邊的路徑都破壞才行。

值得警惕的是,如果您看到某個實例的 "Depth"?為 1 的話,這意味著它直接被 GC root 引用,同時也意味著它永遠(yuǎn)不會被自動回收。

下面是一個示例 Activity,它實現(xiàn)了 LocationListener 接口,高亮部分代碼 "requestLocationUpdates" 將會使用當(dāng)前 Activity 實例來注冊 locationManager。如果您忘記注銷,這個 Activity 就會泄漏。它將永遠(yuǎn)都待在內(nèi)存里,因為位置管理器是一個 GC root,而且永遠(yuǎn)都存在:

您能在 Memory Profiler 中查看這一情況。點擊一個實例,Memory Profiler 將會打開一個面板來顯示誰正在引用這個實例:

我們可以看到位置管理器中的 mListener 正在引用這個 Activity。您可以更進(jìn)一步,通過引用面板導(dǎo)航至堆的引用視圖,它可以讓您驗證這條引用鏈?zhǔn)欠袷悄A(yù)期的,也能幫您理解代碼中是否有泄漏以及哪里有泄漏。

CPU Profiler

和 Memory Profiler 類似,CPU Profiler 提供了從另一個角度記錄和分析應(yīng)用關(guān)鍵性能數(shù)據(jù)的方法。

使用 CPU Profiler,首先要產(chǎn)生一些 CPU 的使用記錄:

? ? 進(jìn)入 Android Studio 中的 CPU Profiler 界面,在您的應(yīng)用已經(jīng)部署的前提下,點擊 "Record"?按鈕;

? ? 在應(yīng)用中進(jìn)行您想要分析的操作;

? ? ?返回 CPU Profiler,點擊 "Stop"?按鈕。

由于最終呈現(xiàn)的數(shù)據(jù)是基于線程組織的,所以去觀察數(shù)據(jù)之前,您應(yīng)該確認(rèn)是否選擇了正確的線程:

我們這里所獲得的 CPU 使用記錄信息,其實是一個 System Trace 實例的調(diào)用棧集合 (下文統(tǒng)稱 "調(diào)用棧")。而就算是很短的 CPU 使用記錄,也會包含巨量的信息,同時這些信息也是人無法讀懂的。所以 CPU Profiler 提供了一些工具來可視化這些數(shù)據(jù)。

Call Chart

在 CPU Profiler 界面下半部,有四個標(biāo)簽頁,分別對應(yīng)四個不同的數(shù)據(jù)圖表,它們分別是: Call Chart、Flame Chart、Top Down 和 Bottom Up。其中的 Call Chart 可能是最直白的一個,它基本上就是一個調(diào)用棧的重新組織和可視化呈現(xiàn):

Call Chart 橫軸就是時間線,用來展示方法開始與結(jié)束的確切時間,縱軸則自上而下展示了方法間調(diào)用和被調(diào)用的關(guān)系。Call Chart 已經(jīng)比原數(shù)據(jù)可讀性高很多,但它仍然不方便發(fā)現(xiàn)那些運行時間很長的代碼,這時我們便需要使用 Flame Chart。

Flame Chart

Flame Chart 提供了一個調(diào)用棧的聚合信息。與 Call Chart 不同的是,它的橫軸顯示的是百分比數(shù)值。由于忽略了時間線信息,F(xiàn)lame Chart 可以展示每次調(diào)用消耗時間占用整個記錄時長的百分比。同時縱軸也被對調(diào)了,在頂部展示的是被調(diào)用者,底部展示的是調(diào)用者。此時的圖表看起來越往上越窄,就好像火焰一樣,因此得名:

Flame Chart 是基于 Call Chart 來重新組織信息的。從 Call Chat 開始,合并相同的調(diào)用棧,以耗時由長至短對調(diào)用棧進(jìn)行排序,就獲得了 Flame Chart:

對比兩種圖表不難看出,左邊的 Call Chart 有詳細(xì)的時間信息,可以展示每次調(diào)用是何時發(fā)生的;右邊的 Flame Chart 所展示的聚合信息,則有助于發(fā)現(xiàn)一個總耗時很長的調(diào)用路徑:

Top Down Tree

前面介紹的兩種圖表,可以幫助我們從兩種角度縱覽全局。而如果我們需要更精確的時間信息,就需要使用 Top Down Tree。在 CPU Profiler 中,Top Down 選項卡展示的是一個數(shù)據(jù)表格,為了便于理解其中各組數(shù)據(jù)的意義,接下來我們會嘗試構(gòu)建一個 Top Down Tree。

構(gòu)建一個 Top Down Tree 并不復(fù)雜。以 Flame Chart 為基礎(chǔ),您只需要從調(diào)用者開始,持續(xù)添加被調(diào)用者作為子節(jié)點,直到整個 Flame Chart 被遍歷一遍,您就獲得了一個 Top Down Tree:

對于每個節(jié)點,我們關(guān)注三個時間信息:

? ?Self Time —— 運行自己的代碼所消耗的時間;

? ?Children Time —— 調(diào)用其他方法的時間;

? ?Total Time —— 前面兩者時間之和。

有了 Top Down Tree,我們能輕易將這三組信息歸納到一個表格之中:

下面我們來看一看這些時間信息是怎么計算的。左手邊是和前面一樣的 Flame Chart 示例。右邊則是一個 Top Down Tree。

我們從 A 節(jié)點開始:

? ?A 消耗了 1 秒鐘來運行自己的代碼,所以 Self Time 是 1;

? ?然后它消耗了 9 秒中去調(diào)用其他方法,這意味著它的 Children Time 是 9;

? ?這樣就一共消耗了 10 秒鐘,Total Time 是 10;

? ?B 和 D 以此類推...

值得注意的是,D 節(jié)點只是調(diào)用了 C,自己沒做任何事,這種情況在方法封裝時很常見。所以 D 的 Children Time 和 Total Time 都是 2。

下面是表格完全展開的狀態(tài)。當(dāng)您在 Android Studio 中分析應(yīng)用時,CPU Profiler 會完成上面所有的計算,您只要理解這些數(shù)字是怎么產(chǎn)生的即可:

對比左右兩邊: Flame Chart 比較便于發(fā)現(xiàn)總耗時很長的調(diào)用鏈,而 Top Down Tree 則方便觀察其中每一步所消耗的精確時間。作為一個表格,Top Down Tree 也支持按單獨維度進(jìn)行排序,這點同樣非常實用。

Bottom Up Tree

當(dāng)您希望方便地找到某個方法的調(diào)用棧時,Bottom Up Tree 就派上用場了。"樹" 如其名,Bottom Up Tree 從底部開始構(gòu)建,這樣我們就能通過在節(jié)點上不斷添加調(diào)用者來反向構(gòu)建出樹。由于每個獨立節(jié)點都可以構(gòu)建出一棵樹,所以這里其實是森林 (Forest):

讓我們再做些計算來搞定這些時間信息。

表格有四行,因為我們有四個樹在森林中。從節(jié)點 C 開始:

? ?Self Time 是 4 + 2 = 6 秒鐘;

? ?C 沒有調(diào)用其他方法,所以 Children Time 是 0;

? ? 前面兩者相加,總時間為 6 秒鐘。

看起來與 Top Bottom Tree 別無二致。接下來展開 C 節(jié)點,計算 C 的調(diào)用者 B 和 D 的情況。

在計算 B 和 D 節(jié)點的相關(guān)時間時,情況與前面的 Top Bottom Tree 有所不同:

? ? 由于我們在構(gòu)建基于 C 節(jié)點的 Bottom Up Tree,所以所有時間信息也都是基于 C 節(jié)點的。這時我們在計算 B 的 Self Time 時,應(yīng)當(dāng)計算 C 被 B 調(diào)用的時間,而不是 B 自身執(zhí)行的時間,這里是 4 秒;對于 D 來說,則是 2 秒。

? ? 由于只有 B 和 D 調(diào)用 C 的方法,它們的 Total Time 之和應(yīng)與 C 的 Total Time 相等。

下一個樹是 B 節(jié)點的 Bottom Up Tree,它的 Self Time 是 3 秒,Children Time 是用來調(diào)用其他方法的時間,這里只有 C,所以是 2 秒。Total Time 永遠(yuǎn)都是前兩者之和。下面便是整個表格展開的樣子:

當(dāng)您想要觀察某個方法如何被調(diào)用,比如這個 nanoTime() 方法時,您可以使用 Bottom Up Tree 并觀察 nanoTime 方法的子節(jié)點列表,通過右邊的時間數(shù)據(jù),您可以找到那個您所感興趣的調(diào)用:

備忘表

前面介紹了四種不同的數(shù)據(jù)圖表,并且還詳細(xì)解釋了一些數(shù)據(jù)是如何被計算出來的。如果您覺得頭緒太多很難記住,沒關(guān)系,下面這個簡明的備忘表就是為您準(zhǔn)備的:

總結(jié)

本文介紹了 Android Studio Profiler 中的兩種數(shù)據(jù)分析工具。

其中 Memory Profiler 可以自動檢測 Activity 和 Fragment 的內(nèi)存泄漏,而通過了解和使用 Memory Profiler 中數(shù)據(jù)分析功能提供的數(shù)據(jù),也可以發(fā)現(xiàn)和解決其他類型的內(nèi)存泄漏問題。

有關(guān) CPU Profiler 則介紹了 Call Chart、Flame Chart、Top Down、Bottom Up 這四種維度的數(shù)據(jù)呈現(xiàn)。

希望這些內(nèi)容能夠幫助您更加了解 Android Profiler。如仍有疑問,歡迎在下方留言。也歡迎通過 Android Studio 反饋使用中遇到的問題。

您也可以通過視頻回顧 2019 Android 開發(fā)者峰會演講 —— 讀懂 Android Studio 分析工具數(shù)據(jù):



讀懂Android Studio分析工具數(shù)據(jù) | ADS視頻_騰訊視頻

點擊這里即刻閱讀更多應(yīng)用性能相關(guān)內(nèi)容

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容