Memory Profiler 是 Android Profiler 中的一個組件,可幫助您識別導致應(yīng)用卡頓它抱、凍結(jié)甚至崩潰的內(nèi)存泄漏和流失。 它顯示一個應(yīng)用內(nèi)存使用量的實時圖表,讓您可以捕獲堆轉(zhuǎn)儲跨跨、強制執(zhí)行垃圾回收以及跟蹤內(nèi)存分配悼做。
一疯特、 為什么應(yīng)分析您的應(yīng)用內(nèi)存
Android 提供一個托管內(nèi)存環(huán)境—當它確定您的應(yīng)用不再使用某些對象時,垃圾回收器會將未使用的內(nèi)存釋放回堆中肛走。 雖然 Android 查找未使用內(nèi)存的方式在不斷改進漓雅,但對于所有 Android 版本,系統(tǒng)都必須在某個時間點短暫地暫停您的代碼朽色。 大多數(shù)情況下邻吞,這些暫停難以察覺。 不過葫男,如果您的應(yīng)用分配內(nèi)存的速度比系統(tǒng)回收內(nèi)存的速度快抱冷,則當收集器釋放足夠的內(nèi)存以滿足您的分配需要時,您的應(yīng)用可能會延遲梢褐。 此延遲可能會導致您的應(yīng)用跳幀旺遮,并使系統(tǒng)明顯變慢赵讯。
盡管您的應(yīng)用不會表現(xiàn)出變慢,但如果存在內(nèi)存泄漏耿眉,則即使應(yīng)用在后臺運行也會保留該內(nèi)存边翼。 此行為會強制執(zhí)行不必要的垃圾回收 Event,因而拖慢系統(tǒng)的內(nèi)存性能鸣剪。 最后组底,系統(tǒng)被迫終止您的應(yīng)用進程以回收內(nèi)存。 然后筐骇,當用戶返回您的應(yīng)用時债鸡,它必須完全重啟。
為幫助防止這些問題铛纬,您應(yīng)使用 Memory Profiler 執(zhí)行以下操作:
- 在時間線中查找可能會導致性能問題的不理想的內(nèi)存分配模式厌均。
- 轉(zhuǎn)儲 Java 堆以查看在任何給定時間哪些對象耗盡了使用內(nèi)存。 長時間進行多個堆轉(zhuǎn)儲可幫助識別內(nèi)存泄漏饺鹃。
- 記錄正常用戶交互和極端用戶交互期間的內(nèi)存分配以準確識別您的代碼在何處短時間分配了過多對象莫秆,或分配了泄漏的對象。
如需了解可減少應(yīng)用內(nèi)存使用的編程做法悔详,請閱讀管理您的應(yīng)用內(nèi)存镊屎。
二、 Memory Profiler 概覽
當您首次打開 Memory Profiler 時茄螃,您將看到一條表示應(yīng)用內(nèi)存使用量的詳細時間線缝驳,并可訪問用于強制執(zhí)行垃圾回收、捕捉堆轉(zhuǎn)儲和記錄內(nèi)存分配的各種工具归苍。
如圖 1 所示用狱,Memory Profiler 的默認視圖包括以下各項:
- 用于強制執(zhí)行垃圾回收 Event 的按鈕。
- 用于捕獲堆轉(zhuǎn)儲的按鈕拼弃。
- 用于記錄內(nèi)存分配情況的按鈕夏伊。 此按鈕僅在連接至運行 Android 7.1 或更低版本的設(shè)備時才會顯示。
- 用于放大/縮小時間線的按鈕吻氧。
- 用于跳轉(zhuǎn)至實時內(nèi)存數(shù)據(jù)的按鈕溺忧。
- Event 時間線,其顯示 Activity 狀態(tài)盯孙、用戶輸入 Event 和屏幕旋轉(zhuǎn) Event鲁森。
- 內(nèi)存使用量時間線,其包含以下內(nèi)容:
- 一個顯示每個內(nèi)存類別使用多少內(nèi)存的堆疊圖表振惰,如左側(cè)的 y 軸以及頂部的彩色鍵所示歌溉。
- 虛線表示分配的對象數(shù),如右側(cè)的 y 軸所示骑晶。
- 用于表示每個垃圾回收 Event 的圖標痛垛。
不過草慧,如果您使用的是運行 Android 7.1 或更低版本的設(shè)備,則默認情況下榜晦,并不是所有分析數(shù)據(jù)均可見冠蒋。 如果您看到一條消息羽圃,其顯示“Advanced profiling is unavailable for the selected process”乾胶,則需要啟用高級分析以查看下列內(nèi)容:
- Event 時間線
- 分配的對象數(shù)
- 垃圾回收 Event
在 Android 8.0 及更高版本上,始終為可調(diào)試應(yīng)用啟用高級分析朽寞。
三识窿、 如何計算內(nèi)存
您在 Memory Profiler(圖 2)頂部看到的數(shù)字取決于您的應(yīng)用根據(jù) Android 系統(tǒng)機制所提交的所有私有內(nèi)存頁面數(shù)。 此計數(shù)不包含與系統(tǒng)或其他應(yīng)用共享的頁面脑融。
內(nèi)存計數(shù)中的類別如下所示:
Java:從 Java 或 Kotlin 代碼分配的對象內(nèi)存喻频。
-
Native:從 C 或 C++ 代碼分配的對象內(nèi)存。
即使您的應(yīng)用中不使用 C++肘迎,您也可能會看到此處使用的一些原生內(nèi)存甥温,因為 Android 框架使用原生內(nèi)存代表您處理各種任務(wù),如處理圖像資源和其他圖形時妓布,即使您編寫的代碼采用 Java 或 Kotlin 語言姻蚓。
Graphics:圖形緩沖區(qū)隊列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內(nèi)存匣沼。 (請注意狰挡,這是與 CPU 共享的內(nèi)存,不是 GPU 專用內(nèi)存释涛。)
Stack: 您的應(yīng)用中的原生堆棧和 Java 堆棧使用的內(nèi)存加叁。 這通常與您的應(yīng)用運行多少線程有關(guān)。
Code:您的應(yīng)用用于處理代碼和資源(如 dex 字節(jié)碼唇撬、已優(yōu)化或已編譯的 dex 碼它匕、.so 庫和字體)的內(nèi)存。
Other:您的應(yīng)用使用的系統(tǒng)不確定如何分類的內(nèi)存窖认。
-
Allocated:您的應(yīng)用分配的 Java/Kotlin 對象數(shù)豫柬。 它沒有計入 C 或 C++ 中分配的對象。
當連接至運行 Android 7.1 及更低版本的設(shè)備時耀态,此分配僅在 Memory Profiler 連接至您運行的應(yīng)用時才開始計數(shù)轮傍。 因此,您開始分析之前分配的任何對象都不會被計入首装。 不過创夜,Android 8.0 附帶一個設(shè)備內(nèi)置分析工具,該工具可記錄所有分配仙逻,因此驰吓,在 Android 8.0 及更高版本上涧尿,此數(shù)字始終表示您的應(yīng)用中待處理的 Java 對象總數(shù)。
與以前的 Android Monitor 工具中的內(nèi)存計數(shù)相比檬贰,新的 Memory Profiler 以不同的方式記錄您的內(nèi)存姑廉,因此,您的內(nèi)存使用量現(xiàn)在看上去可能會更高些翁涤。 Memory Profiler 監(jiān)控的類別更多桥言,這會增加總的內(nèi)存使用量,但如果您僅關(guān)心 Java 堆內(nèi)存葵礼,則“Java”項的數(shù)字應(yīng)與以前工具中的數(shù)值相似号阿。
然而,Java 數(shù)字可能與您在 Android Monitor 中看到的數(shù)字并非完全相同鸳粉,這是因為應(yīng)用的 Java 堆是從 Zygote 啟動的扔涧,而新數(shù)字則計入了為它分配的所有物理內(nèi)存頁面。 因此届谈,它可以準確反映您的應(yīng)用實際使用了多少物理內(nèi)存枯夜。
注:目前,Memory Profiler 還會顯示應(yīng)用中的一些誤報的原生內(nèi)存使用量艰山,而這些內(nèi)存實際上是分析工具使用的湖雹。 對于大約 100000 個對象,最多會使報告的內(nèi)存使用量增加 10MB程剥。 在這些工具的未來版本中劝枣,這些數(shù)字將從您的數(shù)據(jù)中過濾掉。
四织鲸、 查看內(nèi)存分配
內(nèi)存分配顯示內(nèi)存中每個對象是如何分配的舔腾。 具體而言,Memory Profiler 可為您顯示有關(guān)對象分配的以下信息:
- 分配哪些類型的對象以及它們使用多少空間搂擦。
- 每個分配的堆疊追蹤稳诚,包括在哪個線程中。
- 對象在何時被取消分配(僅當使用運行 Android 8.0 或更高版本的設(shè)備時)瀑踢。
如果您的設(shè)備運行 Android 8.0 或更高版本扳还,您可以隨時按照下述方法查看您的對象分配: 只需點擊并按住時間線,并拖動選擇您想要查看分配的區(qū)域(如視頻 1 中所示)橱夭。 不需要開始記錄會話氨距,因為 Android 8.0 及更高版本附帶設(shè)備內(nèi)置分析工具,可持續(xù)跟蹤您的應(yīng)用分配棘劣。
如果您的設(shè)備運行 Android 7.1 或更低版本,則在 Memory Profiler 工具欄中點擊 Record memory allocations
。 記錄時首昔,Android Monitor 將跟蹤您的應(yīng)用中進行的所有分配寡喝。 操作完成后,點擊 Stop recording
(同一個按鈕勒奇;請參閱視頻 2)以查看分配预鬓。
在選擇一個時間線區(qū)域后(或當您使用運行 Android 7.1 或更低版本的設(shè)備完成記錄會話時)赊颠,已分配對象的列表將顯示在時間線下方格二,按類名稱進行分組,并按其堆計數(shù)排序巨税。
注:在 Android 7.1 及更低版本上蟋定,您最多可以記錄 65535 個分配。 如果您的記錄會話超出此限值草添,則記錄中僅保存最新的 65535 個分配。 (在 Android 8.0 及更高版本中扼仲,則沒有實際的限制远寸。)
要檢查分配記錄,請按以下步驟操作:
- 瀏覽列表以查找堆計數(shù)異常大且可能存在泄漏的對象屠凶。 為幫助查找已知類驰后,點擊 Class Name 列標題以按字母順序排序。 然后點擊一個類名稱矗愧。 此時在右側(cè)將出現(xiàn) Instance View 窗格灶芝,顯示該類的每個實例,如圖 3 中所示唉韭。
- 在 Instance View 窗格中夜涕,點擊一個實例。 此時下方將出現(xiàn) Call Stack 標簽属愤,顯示該實例被分配到何處以及哪個線程中女器。
- 在 Call Stack 標簽中,點擊任意行以在編輯器中跳轉(zhuǎn)到該代碼住诸。
默認情況下驾胆,左側(cè)的分配列表按類名稱排列。 在列表頂部贱呐,您可以使用右側(cè)的下拉列表在以下排列方式之間進行切換:
- Arrange by class:基于類名稱對所有分配進行分組丧诺。
- Arrange by package:基于軟件包名稱對所有分配進行分組。
- Arrange by callstack:將所有分配分組到其對應(yīng)的調(diào)用堆棧奄薇。
五驳阎、 捕獲堆轉(zhuǎn)儲
堆轉(zhuǎn)儲顯示在您捕獲堆轉(zhuǎn)儲時您的應(yīng)用中哪些對象正在使用內(nèi)存。 特別是在長時間的用戶會話后,堆轉(zhuǎn)儲會顯示您認為不應(yīng)再位于內(nèi)存中卻仍在內(nèi)存中的對象搞隐,從而幫助識別內(nèi)存泄漏驹愚。 在捕獲堆轉(zhuǎn)儲后,您可以查看以下信息:
- 您的應(yīng)用已分配哪些類型的對象劣纲,以及每個類型分配多少逢捺。
- 每個對象正在使用多少內(nèi)存。
- 在代碼中的何處仍在引用每個對象癞季。
- 對象所分配到的調(diào)用堆棧劫瞳。 (目前,如果您在記錄分配時捕獲堆轉(zhuǎn)儲绷柒,則只有在 Android 7.1 及更低版本中志于,堆轉(zhuǎn)儲才能使用調(diào)用堆棧。)
要捕獲堆轉(zhuǎn)儲废睦,在 Memory Profiler 工具欄中點擊 Dump Java heap
伺绽。 在轉(zhuǎn)儲堆期間,Java 內(nèi)存量可能會暫時增加嗜湃。 這很正常奈应,因為堆轉(zhuǎn)儲與您的應(yīng)用發(fā)生在同一進程中,并需要一些內(nèi)存來收集數(shù)據(jù)购披。
堆轉(zhuǎn)儲顯示在內(nèi)存時間線下杖挣,顯示堆中的所有類類型,如圖 4 所示刚陡。
注:如果您需要更精確地了解轉(zhuǎn)儲的創(chuàng)建時間惩妇,可以通過調(diào)用dumpHprofData()在應(yīng)用代碼的關(guān)鍵點創(chuàng)建堆轉(zhuǎn)儲。
要檢查您的堆筐乳,請按以下步驟操作:
瀏覽列表以查找堆計數(shù)異常大且可能存在泄漏的對象歌殃。 為幫助查找已知類,點擊 Class Name 列標題以按字母順序排序哥童。 然后點擊一個類名稱挺份。 此時在右側(cè)將出現(xiàn) Instance View 窗格,顯示該類的每個實例贮懈,如圖 5 中所示匀泊。
-
在 Instance View 窗格中,點擊一個實例朵你。此時下方將出現(xiàn) References各聘,顯示該對象的每個引用。
或者抡医,點擊實例名稱旁的箭頭以查看其所有字段躲因,然后點擊一個字段名稱查看其所有引用早敬。 如果您要查看某個字段的實例詳情,右鍵點擊該字段并選擇 Go to Instance大脉。
在 References 標簽中搞监,如果您發(fā)現(xiàn)某個引用可能在泄漏內(nèi)存,則右鍵點擊它并選擇 Go to Instance镰矿。 這將從堆轉(zhuǎn)儲中選擇對應(yīng)的實例琐驴,顯示您自己的實例數(shù)據(jù)。
默認情況下秤标,堆轉(zhuǎn)儲不會向您顯示每個已分配對象的堆疊追蹤绝淡。 要獲取堆疊追蹤,在點擊 Dump Java heap 之前苍姜,您必須先開始記錄內(nèi)存分配牢酵。 然后,您可以在 Instance View 中選擇一個實例衙猪,并查看 Call Stack 標簽以及 References 標簽馍乙,如圖 5 所示。不過屈嗤,在您開始記錄分配之前潘拨,可能已分配一些對象,因此饶号,調(diào)用堆棧不能用于這些對象。 包含調(diào)用堆棧的實例在圖標
上用一個“堆椉韭欤”標志表示茫船。 (遺憾的是,由于堆疊追蹤需要您執(zhí)行分配記錄扭屁,因此算谈,您目前無法在 Android 8.0 上查看堆轉(zhuǎn)儲的堆疊追蹤。)
在您的堆轉(zhuǎn)儲中料滥,請注意由下列任意情況引起的內(nèi)存泄漏:
- 長時間引用
Activity
然眼、Context
、View
葵腹、Drawable
和其他對象高每,可能會保持對Activity
或Context
容器的引用。 - 可以保持
Activity
實例的非靜態(tài)內(nèi)部類践宴,如Runnable
鲸匿。 - 對象保持時間超出所需時間的緩存。
在類列表中阻肩,您可以查看以下信息:
- Heap Count:堆中的實例數(shù)带欢。
- Shallow Size:此堆中所有實例的總大小(以字節(jié)為單位)。
- Retained Size:為此類的所有實例而保留的內(nèi)存總大星巧贰(以字節(jié)為單位)吁朦。
在類列表頂部,您可以使用左側(cè)下拉列表在以下堆轉(zhuǎn)儲之間進行切換:
- Default heap:系統(tǒng)未指定堆時渡贾。
- App heap:您的應(yīng)用在其中分配內(nèi)存的主堆逗宜。
- Image heap:系統(tǒng)啟動映像,包含啟動期間預(yù)加載的類剥啤。 此處的分配保證絕不會移動或消失锦溪。
- Zygote heap:寫時復制堆府怯,其中的應(yīng)用進程是從 Android 系統(tǒng)中派生的刻诊。
默認情況下,此堆中的對象列表按類名稱排列牺丙。 您可以使用其他下拉列表在以下排列方式之間進行切換:
- Arrange by class:基于類名稱對所有分配進行分組则涯。
- Arrange by package:基于軟件包名稱對所有分配進行分組。
- Arrange by callstack:將所有分配分組到其對應(yīng)的調(diào)用堆棧冲簿。 此選項僅在記錄分配期間捕獲堆轉(zhuǎn)儲時才有效粟判。 即使如此,堆中的對象也很可能是在您開始記錄之前分配的峦剔,因此這些分配會首先顯示档礁,且只按類名稱列出。
默認情況下吝沫,此列表按 Retained Size 列排序呻澜。 您可以點擊任意列標題以更改列表的排序方式。
在 Instance View 中惨险,每個實例都包含以下信息:
- Depth:從任意 GC 根到所選實例的最短 hop 數(shù)羹幸。
- Shallow Size:此實例的大小。
- Retained Size:此實例支配的內(nèi)存大斜栌洹(根據(jù) dominator 樹)栅受。
六、 將堆轉(zhuǎn)儲另存為 HPROF
在捕獲堆轉(zhuǎn)儲后恭朗,僅當分析器運行時才能在 Memory Profiler 中查看數(shù)據(jù)屏镊。 當您退出分析會話時,您將丟失堆轉(zhuǎn)儲冀墨。 因此闸衫,如果您要保存堆轉(zhuǎn)儲以供日后查看,可通過點擊時間線下方工具欄中的 Export heap dump as HPROF file
诽嘉,將堆轉(zhuǎn)儲導出到一個 HPROF 文件中蔚出。 在顯示的對話框中弟翘,確保使用 .hprof
后綴保存文件。
然后骄酗,通過將此文件拖到一個空的編輯器窗口(或?qū)⑵渫系轿募撕灆谥校┫∮啵梢栽?Android Studio 中重新打開該文件。
要使用其他 HPROF 分析器如 jhat趋翻,您需要將 HPROF 文件從 Android 格式轉(zhuǎn)換為 Java SE HPROF 格式睛琳。 您可以使用 android_sdk/platform-tools/
目錄中提供的 hprof-conv
工具執(zhí)行此操作。 運行包括以下兩個參數(shù)的 hprof-conv
命令:原始 HPROF 文件和轉(zhuǎn)換后 HPROF 文件的寫入位置踏烙。 例如:
hprof-conv heap-original.hprof heap-converted.hprof
七师骗、 分析內(nèi)存的技巧
使用 Memory Profiler 時,您應(yīng)對應(yīng)用代碼施加壓力并嘗試強制內(nèi)存泄漏讨惩。 在應(yīng)用中引發(fā)內(nèi)存泄漏的一種方式是辟癌,先讓其運行一段時間,然后再檢查堆荐捻。 泄漏在堆中可能逐漸匯聚到分配頂部黍少。 不過,泄漏越小处面,您越需要運行更長時間的應(yīng)用才能看到泄漏厂置。
您還可以通過以下方式之一觸發(fā)內(nèi)存泄漏:
- 將設(shè)備從縱向旋轉(zhuǎn)為橫向,然后在不同的 Activity 狀態(tài)下反復操作多次魂角。 旋轉(zhuǎn)設(shè)備經(jīng)常會導致應(yīng)用泄漏 Activity昵济、Context 或 View 對象,因為系統(tǒng)會重新創(chuàng)建 Activity野揪,而如果您的應(yīng)用在其他地方保持對這些對象之一的引用砸紊,系統(tǒng)將無法對其進行垃圾回收。
- 處于不同的 Activity 狀態(tài)時囱挑,在您的應(yīng)用與另一個應(yīng)用之間切換(導航到主屏幕,然后返回到您的應(yīng)用)沼溜。
提示: 您還可以使用 monkeyrunner 測試框架執(zhí)行上述步驟平挑。