Android Studio 如何分析內(nèi)存活動(dòng)
前面的小節(jié)我們學(xué)習(xí)了如何分析 CPU 活動(dòng)。本小節(jié)學(xué)習(xí)如何分析內(nèi)存活動(dòng)。
1. 什么是 Memory Profiler
1.1 Memory Profiler 概覽
Memory Profiler 是 Android Profiler 中的一個(gè)組件,可幫助我們識(shí)別可能會(huì)導(dǎo)致應(yīng)用卡頓、凍結(jié)甚至崩潰的內(nèi)存泄露和內(nèi)存抖動(dòng)。它顯示一個(gè)應(yīng)用內(nèi)存使用量的實(shí)時(shí)圖表,讓我們可以捕獲堆轉(zhuǎn)儲(chǔ)、強(qiáng)制執(zhí)行垃圾回收以及跟蹤內(nèi)存分配。
如果我們的應(yīng)用分配內(nèi)存的速度比系統(tǒng)回收內(nèi)存的速度快,則當(dāng)回收器釋放足夠的內(nèi)存以滿足我們的分配需要時(shí),我們的應(yīng)用可能會(huì)延遲。此延遲可能會(huì)導(dǎo)致我們的應(yīng)用跳幀,并使系統(tǒng)明顯變慢。如果存在內(nèi)存泄露,則即使應(yīng)用在后臺(tái)運(yùn)行也會(huì)保留該內(nèi)存。此行為會(huì)強(qiáng)制執(zhí)行不必要的垃圾回收事件,因而拖慢系統(tǒng)其余部分的內(nèi)存性能。
為幫助防止這些問(wèn)題,我們應(yīng)使用 Memory Profiler 執(zhí)行以下操作:
-
在時(shí)間軸上查找可能會(huì)導(dǎo)致性能問(wèn)題的不理想的內(nèi)存分配模式;
-
轉(zhuǎn)儲(chǔ) Java 堆以查看在任何給定時(shí)間哪些對(duì)象耗盡了內(nèi)存。在很長(zhǎng)一段時(shí)間內(nèi)進(jìn)行多次堆轉(zhuǎn)儲(chǔ)有助于識(shí)別內(nèi)存泄露;
-
記錄正常用戶交互和極端用戶交互期間的內(nèi)存分配,以準(zhǔn)確識(shí)別我們的代碼在何處短時(shí)間內(nèi)分配了過(guò)多對(duì)象,或分配了泄露的對(duì)象。
1.2 打開(kāi) Memory Profiler
要打開(kāi) Memory Profiler,請(qǐng)按以下步驟操作:
依次點(diǎn)擊 View > Tool Windows > Profiler,可以點(diǎn)擊工具欄中的 Profile 圖標(biāo)。
從 Android Profiler 工具欄中選擇要分析的設(shè)備和應(yīng)用進(jìn)程。
點(diǎn)擊 MEMORY 時(shí)間軸上的任意位置以打開(kāi) Memory Profiler。
當(dāng)我們首次打開(kāi) Memory Profiler 時(shí),我們將看到一條表示應(yīng)用內(nèi)存使用量的詳細(xì)時(shí)間軸,并可使用各種工具來(lái)強(qiáng)制執(zhí)行垃圾回收、捕獲堆轉(zhuǎn)儲(chǔ)以及記錄內(nèi)存分配。
-
用于強(qiáng)制執(zhí)行垃圾回收事件的按鈕;
-
用于捕獲堆轉(zhuǎn)儲(chǔ)的按鈕;
-
用于指定分析器多久捕獲一次內(nèi)存分配的下拉菜單。選擇適當(dāng)?shù)倪x項(xiàng)可幫助我們?cè)诜治鰰r(shí)提高應(yīng)用性能;
-
用于縮放時(shí)間軸的按鈕;
-
用于跳轉(zhuǎn)到實(shí)時(shí)內(nèi)存數(shù)據(jù)的按鈕;
-
事件時(shí)間軸,顯示活動(dòng)狀態(tài)、用戶輸入事件和屏幕旋轉(zhuǎn)事件;
-
內(nèi)存使用量時(shí)間軸,它會(huì)顯示以下內(nèi)容:
-
一個(gè)堆疊圖表,顯示每個(gè)內(nèi)存類(lèi)別當(dāng)前使用多少內(nèi)存,如左側(cè)的 y 軸以及頂部的彩色鍵所示;
-
一條虛線,表示分配的對(duì)象數(shù),如右側(cè)的 y 軸所示;
-
每個(gè)垃圾回收事件的圖標(biāo)。
-
2. 如何計(jì)算內(nèi)存
我們?cè)?Memory Profiler 頂部看到的數(shù)字基于我們的應(yīng)用根據(jù) Android 系統(tǒng)機(jī)制所提交的所有私有內(nèi)存頁(yè)面。此計(jì)數(shù)不包含與系統(tǒng)或其他應(yīng)用共享的頁(yè)面。
內(nèi)存計(jì)數(shù)中的類(lèi)別如下:
-
Java:從 Java 或 Kotlin 代碼分配的對(duì)象的內(nèi)存;
-
Native:從 C 或 C++ 代碼分配的對(duì)象的內(nèi)存;
-
Graphics:圖形緩沖區(qū)隊(duì)列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內(nèi)存;(請(qǐng)注意,這是與 CPU 共享的內(nèi)存,不是 GPU 專(zhuān)用內(nèi)存。)
-
Stack:我們的應(yīng)用中的原生堆棧和 Java 堆棧使用的內(nèi)存。通常與我們的應(yīng)用運(yùn)行多少線程有關(guān);
-
Code:我們的應(yīng)用用于處理代碼和資源(如 dex 字節(jié)碼、經(jīng)過(guò)優(yōu)化或編譯的 dex 代碼、.so 庫(kù)和字體)的內(nèi)存;
-
Others:我們的應(yīng)用使用的系統(tǒng)不確定如何分類(lèi)的內(nèi)存;
-
Allocated:我們的應(yīng)用分配的 Java/Kotlin 對(duì)象數(shù)。此數(shù)字沒(méi)有計(jì)入 C 或 C++ 中分配的對(duì)象。
3. 內(nèi)存分配
3.1 如何查看內(nèi)存分配
內(nèi)存分配為我們顯示內(nèi)存中的每個(gè) Java 對(duì)象和 JNI 引用是如何分配的。具體而言,Memory Profiler 可為我們顯示有關(guān)對(duì)象分配的以下信息:
-
分配了哪些類(lèi)型的對(duì)象以及它們使用多少空間;
-
每個(gè)分配的堆棧軌跡,包括在哪個(gè)線程中;
-
對(duì)象在何時(shí)被取消分配。
如果我們的設(shè)備搭載的是 Android 8.0 或更高版本,我們可以隨時(shí)查看對(duì)象分配,具體操作步驟如下:在時(shí)間軸上拖動(dòng)以選擇要查看哪個(gè)區(qū)域的分配。不需要開(kāi)始記錄會(huì)話,因?yàn)?Android 8.0 及更高版本附帶設(shè)備內(nèi)置分析工具,可持續(xù)跟蹤我們的應(yīng)用分配。
如果我們的設(shè)備搭載的是 Android 7.1 或更低版本,請(qǐng)點(diǎn)擊 Memory Profiler 工具欄中的 Record memory allocations 圖標(biāo)。記錄時(shí),Memory Profiler 會(huì)跟蹤我們的應(yīng)用中發(fā)生的所有分配。完成后,請(qǐng)點(diǎn)擊 Stop recording 圖標(biāo)以查看分配。
3.2 檢查分析分配記錄
選擇時(shí)間軸的某個(gè)區(qū)域后(或者使用搭載 Android 7.1 或更低版本的設(shè)備完成記錄會(huì)話后),已分配對(duì)象的列表將顯示在時(shí)間軸下方,按類(lèi)名稱進(jìn)行分組,并按其堆計(jì)數(shù)排序。
要檢查分配記錄,請(qǐng)按以下步驟操作:
-
瀏覽列表以查找堆計(jì)數(shù)異常大且可能存在泄露的對(duì)象。為幫助查找已知類(lèi),點(diǎn)擊 Class Name 列標(biāo)題以按字母順序排序。然后,點(diǎn)擊一個(gè)類(lèi)名稱。此時(shí)右側(cè)將出現(xiàn) Instance View 窗格,顯示該類(lèi)的每個(gè)實(shí)例;
-
在 Instance View 窗格中,點(diǎn)擊一個(gè)實(shí)例。此時(shí)下方將出現(xiàn) Call Stack 標(biāo)簽頁(yè),顯示該實(shí)例被分配到何處以及在哪個(gè)線程中;
-
在 Call Stack 標(biāo)簽頁(yè)中,右鍵點(diǎn)擊任意行并選擇 Jump to Source,以在編輯器中打開(kāi)該代碼。
我們可以使用已分配對(duì)象列表上方的兩個(gè)菜單來(lái)選擇要檢查的堆以及如何組織數(shù)據(jù)。從左側(cè)的菜單中,選擇要檢查的堆:
-
default heap:當(dāng)系統(tǒng)未指定堆時(shí);
-
image heap:系統(tǒng)啟動(dòng)映像,包含啟動(dòng)期間預(yù)加載的類(lèi)。此處的分配保證絕不會(huì)移動(dòng)或消失;
-
zygote heap:寫(xiě)時(shí)復(fù)制堆,其中的應(yīng)用進(jìn)程是從 Android 系統(tǒng)中派生的;
-
app heap:我們的應(yīng)用在其中分配內(nèi)存的主堆;
-
JNI heap:顯示 Java 原生接口 (JNI) 引用被分配和釋放到什么位置的堆。
從右側(cè)的菜單中,選擇如何安排分配:
-
Arrange by class:根據(jù)類(lèi)名稱對(duì)所有分配進(jìn)行分組。這是默認(rèn)選項(xiàng);
-
Arrange by package:根據(jù)軟件包名稱對(duì)所有分配進(jìn)行分組;
-
Arrange by callstack:將所有分配分組到其對(duì)應(yīng)的調(diào)用堆棧;
3.2 查看全局 JNI 引用
JNI 引用由原生代碼進(jìn)行管理,因此原生代碼使用的 Java 對(duì)象可能會(huì)保持活動(dòng)狀態(tài)太長(zhǎng)時(shí)間。如果丟棄了 JNI 引用而未先明確將其刪除,Java 堆上的某些對(duì)象可能會(huì)變得無(wú)法訪問(wèn)。此外,還可能會(huì)達(dá)到全局 JNI 引用限制。
要排查此類(lèi)問(wèn)題,請(qǐng)使用 Memory Profiler 中的 JNI heap 視圖來(lái)瀏覽所有全局 JNI 引用,并按 Java 類(lèi)型和原生調(diào)用堆棧對(duì)其進(jìn)行過(guò)濾。借助此信息,我們可以了解創(chuàng)建和刪除全局 JNI 引用的時(shí)間和位置。
在我們的應(yīng)用運(yùn)行時(shí),選擇我們要檢查的一部分時(shí)間軸,然后從類(lèi)列表上方的下拉菜單中選擇 JNI heap。 我們隨后可以像往常一樣檢查堆中的對(duì)象,還可以雙擊 Allocation Call Stack 標(biāo)簽頁(yè)中的對(duì)象,以查看在代碼中將 JNI 引用分配和釋放到了什么位置,如下圖所示。
Tips:要檢查應(yīng)用的 JNI 代碼的內(nèi)存分配,必須將應(yīng)用部署到搭載 Android 8.0 或更高版本的設(shè)備上。
4. 堆轉(zhuǎn)儲(chǔ)
堆轉(zhuǎn)儲(chǔ)顯示在我們捕獲堆轉(zhuǎn)儲(chǔ)時(shí)我們的應(yīng)用中哪些對(duì)象正在使用內(nèi)存。特別是在長(zhǎng)時(shí)間的用戶會(huì)話后,堆轉(zhuǎn)儲(chǔ)會(huì)顯示我們認(rèn)為不應(yīng)再位于內(nèi)存中卻仍在內(nèi)存中的對(duì)象,從而幫助識(shí)別內(nèi)存泄露。
捕獲堆轉(zhuǎn)儲(chǔ)后,我們可以查看以下信息:
-
我們的應(yīng)用分配了哪些類(lèi)型的對(duì)象,以及每種對(duì)象有多少;
-
每個(gè)對(duì)象當(dāng)前使用多少內(nèi)存;
-
在代碼中的什么位置保持著對(duì)每個(gè)對(duì)象的引用;
-
對(duì)象所分配到的調(diào)用堆棧。
4.1 如何捕獲堆轉(zhuǎn)儲(chǔ)
要捕獲堆轉(zhuǎn)儲(chǔ),請(qǐng)點(diǎn)擊 Memory Profiler 工具欄中的 Dump Java heap 圖標(biāo)。 在轉(zhuǎn)儲(chǔ)堆期間,Java 內(nèi)存量可能會(huì)暫時(shí)增加。 這很正常,因?yàn)槎艳D(zhuǎn)儲(chǔ)與我們的應(yīng)用發(fā)生在同一進(jìn)程中,并需要一些內(nèi)存來(lái)收集數(shù)據(jù)。
堆轉(zhuǎn)儲(chǔ)出現(xiàn)在內(nèi)存時(shí)間軸下方,顯示堆中的所有類(lèi)類(lèi)型,如下圖所示。
在類(lèi)列表中,我們可以查看以下信息:
-
Allocations:堆中的分配數(shù);
-
Native Size:此對(duì)象類(lèi)型使用的原生內(nèi)存總量(以字節(jié)為單位)。只有在使用 Android 7.0 及更高版本時(shí),才會(huì)看到此列;
-
Shallow Size:此對(duì)象類(lèi)型使用的 Java 內(nèi)存總量(以字節(jié)為單位);
-
Retained Size:為此類(lèi)的所有實(shí)例而保留的內(nèi)存總大?。ㄒ宰止?jié)為單位);
點(diǎn)擊一個(gè)類(lèi)名稱可在右側(cè)打開(kāi) Instance View 窗口。列出的每個(gè)實(shí)例都包含以下信息:
-
Depth:從任意 GC 根到選定實(shí)例的最短跳數(shù);
-
Native Size:原生內(nèi)存中此實(shí)例的大小。 只有在使用 Android 7.0 及更高版本時(shí),才會(huì)看到此列;
-
Shallow Size:Java 內(nèi)存中此實(shí)例的大小;
-
Retained Size:此實(shí)例所支配內(nèi)存的大小;
要檢查應(yīng)用的堆,請(qǐng)按以下步驟操作:
-
瀏覽列表以查找堆計(jì)數(shù)異常大且可能存在泄露的對(duì)象。為幫助查找已知類(lèi),點(diǎn)擊 Class Name 列標(biāo)題以按字母順序排序。然后,點(diǎn)擊一個(gè)類(lèi)名稱。此時(shí)右側(cè)將出現(xiàn) Instance View 窗格,顯示該類(lèi)的每個(gè)實(shí)例;
-
在 Instance View 窗格中,點(diǎn)擊一個(gè)實(shí)例。此時(shí)下方將出現(xiàn) References 標(biāo)簽頁(yè),顯示對(duì)該對(duì)象的每個(gè)引用;
-
在 References 標(biāo)簽頁(yè)中,如果我們發(fā)現(xiàn)某個(gè)引用可能在泄露內(nèi)存,請(qǐng)右鍵點(diǎn)擊它并選擇 Go to Instance。這樣會(huì)從堆轉(zhuǎn)儲(chǔ)中選擇相應(yīng)的實(shí)例,從而向我們顯示它自己的實(shí)例數(shù)據(jù)。
4.2 HPROF 文件
捕獲堆轉(zhuǎn)儲(chǔ)后,只有在 Memory Profiler 正在運(yùn)行時(shí),才能在該分析器中查看數(shù)據(jù)。當(dāng)我們退出分析會(huì)話時(shí),會(huì)丟失堆轉(zhuǎn)儲(chǔ)。因此,如果我們要保存堆轉(zhuǎn)儲(chǔ)以供日后查看,請(qǐng)將其導(dǎo)出到 HPROF 文件。
Sessions 窗格中每個(gè) Heap Dump 條目的右側(cè)都有一個(gè) Export Heap Dump 按鈕。在隨即顯示的 Export As 對(duì)話框中,使用 .hprof 文件擴(kuò)展名保存文件。
要使用其他 HPROF 分析器(如 jhat),我們需要將 HPROF 文件從 Android 格式轉(zhuǎn)換為 Java SE HPROF 格式。 我們可以使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執(zhí)行此操作。運(yùn)行包含兩個(gè)參數(shù)(即原始 HPROF 文件和轉(zhuǎn)換后 HPROF 文件的寫(xiě)入位置)的 hprof-conv 命令。例如:
hprof-conv heap-original.hprof heap-converted.hprof
5. 小結(jié)
本節(jié)課程我們主要學(xué)習(xí)了如何分析內(nèi)存活動(dòng)。本節(jié)課程的重點(diǎn)如下:
- 掌握如何查看內(nèi)存分配和堆;
- 掌握如何檢查分析內(nèi)存數(shù)據(jù)。