為了幫助開發(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ù):
點擊這里即刻閱讀更多應(yīng)用性能相關(guān)內(nèi)容
