目錄
前言
Android 存在內(nèi)存回收機(jī)制,當(dāng)它確定應(yīng)用不再使用某些對(duì)象時(shí),垃圾回收器會(huì)將未使用的內(nèi)存釋放回堆中。 雖然 Android 查找未使用內(nèi)存的方式在不斷改進(jìn),但對(duì)于所有 Android 版本,系統(tǒng)都必須在某個(gè)時(shí)間點(diǎn)短暫地暫停你寫的代碼。 大多數(shù)情況下,這些暫停難以察覺。 但是,如果你的應(yīng)用分配內(nèi)存的速度比系統(tǒng)回收內(nèi)存的速度快,那么當(dāng)釋放足夠的內(nèi)存以滿足應(yīng)用的分配需要時(shí),應(yīng)用就可能出現(xiàn)延遲。 這樣可能會(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)存性能。 最后,系統(tǒng)被迫終止你的應(yīng)用進(jìn)程以回收內(nèi)存。 然后,當(dāng)用戶返回你的應(yīng)用時(shí),就必須完全重啟
為幫助防止這些問題,我們可以使用Memory Profiler
- 實(shí)時(shí)圖表展示應(yīng)用內(nèi)存使用量
- 識(shí)別內(nèi)存泄漏、抖動(dòng)
- 提供捕獲堆轉(zhuǎn)儲(chǔ)、強(qiáng)制GC以及跟蹤內(nèi)存分配的能力
Memory Profiler 概覽
如圖 1 所示,Memory Profiler 的默認(rèn)視圖包括以下各項(xiàng):
強(qiáng)制執(zhí)行垃圾回收
堆轉(zhuǎn)儲(chǔ),把內(nèi)存信息通過文件的方式保存下來,可以進(jìn)行分析
記錄內(nèi)存分配情況, 此按鈕僅在連接至運(yùn)行 Android 7.1 或更低版本的設(shè)備時(shí)才會(huì)顯示
放大/縮小時(shí)間線
跳轉(zhuǎn)至實(shí)時(shí)內(nèi)存數(shù)據(jù)
Event 時(shí)間線,顯示 Activity 狀態(tài)、用戶輸入 Event 和屏幕旋轉(zhuǎn) Event
-
內(nèi)存使用量時(shí)間線,其包含以下內(nèi)容:
一個(gè)顯示每個(gè)內(nèi)存類別使用多少內(nèi)存的堆疊圖表,如左側(cè)的 y 軸以及頂部的彩色鍵所示
虛線表示分配的對(duì)象數(shù),如右側(cè)的 y 軸所示
用于表示每個(gè)垃圾回收 Event 的圖標(biāo)
如何計(jì)算內(nèi)存占用
內(nèi)存計(jì)數(shù)中的類別如下所示:
Java:從 Java 或 Kotlin 代碼分配的對(duì)象內(nèi)存
-
Native:從 C 或 C++ 代碼分配的對(duì)象內(nèi)存
即使你的應(yīng)用中不使用 C++,你也可能會(huì)看到此處使用的一些原生內(nèi)存,因?yàn)?Android 框架使用原生內(nèi)存代表您處理各種任務(wù),如處理圖像資源和其他圖形時(shí),即使你編寫的代碼采用 Java 或 Kotlin 語言
Graphics:圖形緩沖區(qū)隊(duì)列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內(nèi)存 (請(qǐng)注意,這是與 CPU 共享的內(nèi)存,不是 GPU 專用內(nèi)存)
Stack: 應(yīng)用中的原生堆棧和 Java 堆棧使用的內(nèi)存。 這通常與您的應(yīng)用運(yùn)行多少線程有關(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 對(duì)象數(shù)。 它沒有計(jì)入 C 或 C++ 中分配的對(duì)象
當(dāng)連接至運(yùn)行 Android 7.1 及更低版本的設(shè)備時(shí),此分配僅在 Memory Profiler 連接至你運(yùn)行的應(yīng)用時(shí)才開始計(jì)數(shù)。 因此,你開始分析之前分配的任何對(duì)象都不會(huì)被計(jì)入。 不過,Android 8.0 附帶一個(gè)設(shè)備內(nèi)置分析工具,該工具可記錄所有分配,因此,在 Android 8.0 及更高版本上,此數(shù)字始終表示你的應(yīng)用中待處理的 Java 對(duì)象總數(shù)
Java 數(shù)字可能與你在 Android Monitor 中看到的數(shù)字并非完全相同,這是因?yàn)閼?yīng)用的 Java 堆是從 Zygote 啟動(dòng)的,而新數(shù)字則計(jì)入了為它分配的所有物理內(nèi)存頁面。 因此,它可以準(zhǔn)確反映你的應(yīng)用實(shí)際使用了多少物理內(nèi)存
查看內(nèi)存分配
要檢查內(nèi)存分配記錄,可以按以下步驟操作:
- 瀏覽列表以查找堆計(jì)數(shù)異常大且可能存在泄漏的對(duì)象。 點(diǎn)擊 Class Name 列標(biāo)題以按字母順序排序。 然后點(diǎn)擊一個(gè)類名稱。 此時(shí)在右側(cè)將出現(xiàn) Instance View 窗格,顯示該類的每個(gè)實(shí)例,如圖 3 中所示
- 在 Instance View 窗格中,點(diǎn)擊一個(gè)實(shí)例。 此時(shí)下方將出現(xiàn) Call Stack 標(biāo)簽,顯示該實(shí)例被分配到何處以及哪個(gè)線程中
- 在 Call Stack 標(biāo)簽中,點(diǎn)擊任意行以在編輯器中跳轉(zhuǎn)到該代碼
默認(rèn)情況下,左側(cè)的分配列表按類名稱排列。 在列表頂部,你可以使用右側(cè)的下拉列表在以下排列方式之間進(jìn)行切換:
- Arrange by class:基于類名稱對(duì)所有分配進(jìn)行分組
- Arrange by package:基于軟件包名稱對(duì)所有分配進(jìn)行分組
- Arrange by callstack:將所有分配分組到其對(duì)應(yīng)的調(diào)用堆棧
捕獲堆轉(zhuǎn)儲(chǔ)
堆轉(zhuǎn)儲(chǔ)顯示在您捕獲堆轉(zhuǎn)儲(chǔ)時(shí)您的應(yīng)用中哪些對(duì)象正在使用內(nèi)存
要捕獲堆轉(zhuǎn)儲(chǔ),在 Memory Profiler 工具欄中點(diǎn)擊 Dump Java heap
注:如果您需要更精確地了解轉(zhuǎn)儲(chǔ)的創(chuàng)建時(shí)間,可以通過調(diào)用
Debug.dumpHprofData()
在應(yīng)用代碼的關(guān)鍵點(diǎn)創(chuàng)建堆轉(zhuǎn)儲(chǔ)
要檢查堆信息,請(qǐng)按以下步驟操作:
- 瀏覽列表以查找堆計(jì)數(shù)異常大且可能存在泄漏的對(duì)象。 為幫助查找已知類,點(diǎn)擊 Class Name 列標(biāo)題以按字母順序排序。 然后點(diǎn)擊一個(gè)類名稱。 此時(shí)在右側(cè)將出現(xiàn) Instance View 窗格,顯示該類的每個(gè)實(shí)例,如圖 5 中所示
- 在Instance View窗格中,點(diǎn)擊一個(gè)實(shí)例。此時(shí)下方將出現(xiàn)References,顯示該對(duì)象的每個(gè)引用
- 在 References 標(biāo)簽中,如果您發(fā)現(xiàn)某個(gè)引用可能在泄漏內(nèi)存,則右鍵點(diǎn)擊它并選擇 Go to Instance
在堆轉(zhuǎn)儲(chǔ)中,請(qǐng)注意由下列任意情況引起的內(nèi)存泄漏:
- 長時(shí)間引用
Activity
、Context
、View
、Drawable
和其他對(duì)象,可能會(huì)保持對(duì)Activity
或Context
容器的引用 - 可以保持
Activity
實(shí)例的非靜態(tài)內(nèi)部類,如Runnable
- 對(duì)象保持時(shí)間超出所需時(shí)間的緩存
在類列表中,你可以查看以下信息:
- Heap Count:堆中的實(shí)例數(shù)
- Shallow Size:此堆中所有實(shí)例的總大小(以字節(jié)為單位)
- Retained Size:為此類的所有實(shí)例而保留的內(nèi)存總大小(以字節(jié)為單位)
在類列表頂部,你可以使用左側(cè)下拉列表在以下堆轉(zhuǎn)儲(chǔ)之間進(jìn)行切換:
- Default heap:系統(tǒng)未指定堆時(shí)
- App heap:應(yīng)用在其中分配內(nèi)存的主堆
- Image heap:系統(tǒng)啟動(dòng)映像,包含啟動(dòng)期間預(yù)加載的類。 此處的分配保證絕不會(huì)移動(dòng)或消失
- Zygote heap:寫時(shí)復(fù)制堆,其中的應(yīng)用進(jìn)程是從 Android 系統(tǒng)中派生的
在 Instance View 中,每個(gè)實(shí)例都包含以下信息:
- Depth:從任意 GC 根到所選實(shí)例的最短 hop 數(shù)
- Shallow Size:此實(shí)例的大小
- Retained Size:此實(shí)例支配的內(nèi)存大小
分析內(nèi)存的技巧
使用 Memory Profiler 時(shí),你可以應(yīng)用代碼施加壓力并嘗試強(qiáng)制內(nèi)存泄漏。 在應(yīng)用中引發(fā)內(nèi)存泄漏的一種方式是,先讓其運(yùn)行一段時(shí)間,然后再檢查堆。 泄漏在堆中可能逐漸匯聚到分配頂部。 不過,泄漏越小,你越需要運(yùn)行更長時(shí)間的應(yīng)用才能看到泄漏
您還可以通過以下方式之一觸發(fā)內(nèi)存泄漏:
- 將設(shè)備從縱向旋轉(zhuǎn)為橫向,然后在不同的 Activity 狀態(tài)下反復(fù)操作多次。 旋轉(zhuǎn)設(shè)備經(jīng)常會(huì)導(dǎo)致應(yīng)用泄漏
Activity
、Context
或View
對(duì)象,因?yàn)橄到y(tǒng)會(huì)重新創(chuàng)建Activity
,而如果您的應(yīng)用在其他地方保持對(duì)這些對(duì)象之一的引用,系統(tǒng)將無法對(duì)其進(jìn)行垃圾回收 - 處于不同的 Activity 狀態(tài)時(shí),在您的應(yīng)用與另一個(gè)應(yīng)用之間切換(導(dǎo)航到主屏幕,然后返回到您的應(yīng)用)