一、前言
內存問題的分析工具有很多種,這裏我們選擇常見的三種進行學習和實戰。
二、工具使用
2.1 Memory Profiler
如上圖所示,我們可以獲取指定時間的當前內存分配情況。在左下角的類列表中,您可以查看以下信息:
- Allocations:堆中的分配數。
- Native Size:此對象類型使用的原生內存總量(以字節爲單位)。只有在使用 Android 7.0 及更高版本時,纔會看到此列。您會在此處看到採用 Java 分配的某些對象的內存,因爲 Android 對某些框架類(如 Bitmap)使用原生內存。
- Shallow Size:此對象類型使用的 Java 內存總量(以字節爲單位)。
- Retained Size:爲此類的所有實例而保留的內存總大小(以字節爲單位)。
點擊一個類名稱可在右側打開 Instance View 窗口(如圖 6 所示)。列出的每個實例都包含以下信息:
- Depth:從任意 GC 根到選定實例的最短跳數。
- Native Size:原生內存中此實例的大小。 只有在使用 Android 7.0 及更高版本時,纔會看到此列。
- Shallow Size:Java 內存中此實例的大小。
- Retained Size:此實例所支配內存的大小。
我們也可以通過 堆轉儲導出到 HPROF 文件:
並通過 Export 進行保存。這裏我們保存爲 1.hprof 文件,後面會用到。
關於 Memory Profiler 更詳細的操作說明推薦看官網的文檔學習。
2.2 MAT
MAT(全稱 Eclipse Memory Analysis Tools)是一個分析 Java堆數據的專業工具,可以計算出內存中對象的實例數量、佔用空間大小、引用關係等,看看是誰阻止了垃圾收集器的回收工作,從而定位內存泄漏的原因。下載地址:https://www.eclipse.org/mat/。
在 Memory Profiler 中導出的 hprof 文件需要進行一個轉換之後才能在 mat 工具中打開,該工具在 Android SDK 工具包裏面,具體的路徑在 …/sdk/platform-tools,裏面有一個hprof-conv工具,使用如下的命令
hprof-conv 舊的hprof文件路徑 新生成的hprof文件路徑
在 mat 中打開這個新生成的 hprof 文件即可:
- Histogram:直方圖,可以列出內存中每個對象的名字、數量以及大小。
- Dominator Tree:會將所有內存中的對象按大小進行排序,並且我們可以分析對象之間的引用結構。
在 Histogram 視圖中,我們可以搜索 Activity 相關的對象。點擊右鍵可以看到 List objects:
- List objects -> with outgoing references :表示該對象的出節點(被該對象引用的對象)
- List objects -> with incoming references:表示該對象的入節點(引用到該對象的對象)
還可以看到 Merge Shortest Paths to GC Roots:
表示查看 GC Roots 到這個對象的路徑,右邊是要選擇的引用類型,一般我們選擇導出第二個即排除軟、弱、虛引用,只保留強引用。
2.3 CPU Profiler
手機上運行要分析 APP,再在 AS 的 Profiler 窗口選擇要調試的進程,打開 CPU Profiler。然後在AS上點擊 Record ,再點擊 Stop recording。得到如下圖所示信息。
Call Chart 標籤提供函數跟蹤的圖形表示形式,其中水平軸表示函數耗費的時間,垂直軸顯示其被調用者。
- 對系統 API 的函數調用顯示爲橙色
- 對應用自有函數的調用顯示爲綠色
- 對第三方 API(包括 Java 語言 API)的函數調用顯示爲藍色。
更詳細的使用可以看Android性能優化之CPU Profiler。
三、實戰
3.1 內存泄漏解決實戰
Android 內存泄漏指的是進程中某些對象(垃圾對象)已經沒有使用價值了, 但是它們卻可以直接或間接地引用到gc roots導致無法被GC回收。無用的對象佔據着內存空間,使得實際可使用內存變小,形象地說法就是內存泄漏了。
在 Memory Profiler 上表現爲可用內存逐漸變少或者內存抖動。其中內存抖動可能是因爲代碼邏輯問題導致內存被不斷地進行分配和回收,當然一個地方它的內存一直在抖動, 還有可能是由於內存泄漏引起的, 比如說,內存泄漏導致可用內存逐漸減少, 這時候系統爲了增加可用內存,就會一直不斷地進行GC, 導致內存一直在抖動。
3.1.1 Activity 溢出
Activity 的溢出比較容易分析,下面模擬一段 Activity 無法回收的代碼:
public class MemoryLeakActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_shake);
processBiz();
}
private void processBiz() {
Handler mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d("MainActivity", "------postDelayed------");
}
}, 800000L);
}
}
在 MainActivity 中我們可以再反覆進入這個 MemoryLeakActivity,然後在 Memory Profiler 中強制 GC,等待幾秒後觀察堆內存信息,篩選出 Activity,就可以看到指定 Activity 的實例了。
也可以堆轉儲然後到 MAT 中去分析:
找出 GC Root 引用:
3.1.2 對象溢出
舉例一個典型的分析內存泄漏的過程:
- 使用 Heap查看當前堆大小爲 23.00M
- 添加一個頁後堆大小變爲 23.40M
- 將添加的一個頁刪除,堆大小爲 23.40M
- 多次操作,結果仍相似,說明添加/刪除頁存在內存泄漏 (也應注意排除其它因素的影響)
- Dump 出操作前後的 hprof 文件 (1.hprof,2.hprof),用 mat打開,並得到 histgram結果
- 使用 HomePage字段過濾 histgram結果,並列出該類的對象實例列表,看到兩個表中的對象集合大小不同,操作後比操作前多出一個 HomePage,說明確實存在泄漏
- 將兩個列表進行對比,找出多出的一個對象,用查找 GC Root的方法找出是誰串起了這條引用線路,定位結束
很多時候堆增大是 Bitmap引起的,Bitmap在 Histogram中的類型是 byte [],對比兩個 Histogram 中的 byte[] 對象就可以找出哪些 Bitmap有差異。另外多使用排序功能,對找出差異很有用。
對象溢出一般是用對象被 static 修飾而無法釋放。爲了更有效率的找出內存泄露的對象,一般會獲取兩個堆轉儲文件(先dump一個,隔段時間再dump一個),通過對比後的結果可以很方便定位。找出差異後用 Histogram 查詢的方法找出 GC Root,定位到具體的某個對象上。
3.2 內存抖動解決實戰
內存抖動即內存頻繁分配和回收導致內存不穩定。頻繁創建對象,導致內存不足或者產生內存碎片,內存碎片即內存不連續,有內存空洞, 某兩個正在使用的內存中間有一個間隔, 這個間隔雖然也被算在可用內存裏面, 但實際上因爲它過小, 當我們申請內存的時候,經常是需要申請一定量的連續內存, 而這些碎片小內存不符合要求,是不能拿來使用的。頻繁GC會導致卡頓,隨後不連續的內存片無法被分配,可分配的內存減少,便最終可能導致OOM。
下面我們來模擬一段導致內存抖動的代碼:
public class MemoryShakeActivity extends AppCompatActivity {
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 創造內存抖動(編寫耗內存的操作)
for (int index = 0; index <= 100; index++) {
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0, 30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_shake);
findViewById(R.id.bt_memory).setOnClickListener(v -> mHandler.sendEmptyMessage(0));
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
運行起來後,內存是平穩的:
點擊按鈕後,開始出現鋸齒狀:
可以看到上面的連續垃圾桶就代表不斷地在GC,我們獲取一個區域的內存信息:
我們可以看到鋸齒的位置,String[] 的分配是相對比較大的; Shallow Size是該類型實例的總大小(以字節爲單位)。於是現在可以鎖定,String[] 是最可疑的引起內存抖動的原因, 點擊左邊的String[]行項,工具會在右邊,彈出另外一個窗口, 窗口上邊是分配出來的該類型的所有實例(<工具右上>), 點擊任意一個實例, 又會在下邊彈出一個該實例的內存分配的堆棧信息(<工具右下>——Allocation Call Stack), 信息即這個實例佔有的這塊內存是在哪裏分配的。
我們可以看到, Memory Profiler 工具的右下表格顯示出來了右上角選中的對應的實例的分配內存的位置—— “handlerMessage方法中,MemoryShakeActivity 文件的第23行”。 右鍵選中 Jump to Source, 直接在IDE代碼編輯界面,跳轉追蹤到,可疑誘因String[]的創建源碼處 / 位置。 然後便發現原因,進行代碼的修改。
也可以通過 CPU Profiler 進行分析,運行程序以及 CPU Profiler 工具, 使用 Record 按鈕開始記錄某一段CPU執行的時間, 接着點擊 Stop 停止對這段時間記錄,跟蹤這一段 CPU執行的時間, 上述 Record 記錄完畢之後會在工具下側彈出圖表界面,如Call Chart,依據這些圖表數據如果發現某一段(應用自有函數的調用)代碼(即綠色的條形段)在反覆地被執行,便是內存抖動的位置。
雙擊 Call Chart 中的一段綠色條形, 可以直接在IDE代碼編輯界面跳轉追蹤到可疑誘因 String[] 的分配執行函數源碼處 / 位置, 然後便發現原因,進行代碼的修改。
內存抖動的解決技巧:重點關注循環或者頻繁調用的地方, 因爲內存抖動就是內存在被不斷地回收及分配, 這種情況的話經常是出現在循環或者頻繁調用的地方。
- 使用 Memory Profiler 初步排查該工具的圖表顯示方式非常直觀,可以清楚地看到內存的使用情況。也可以很方便地發現 APP 在使用過程中,內存分配圖形是不是一個鋸齒狀,有沒有內存抖動的表現。
- 使用 Memory Profiler 的堆轉儲 / 跟蹤分配內存 功能,藉助 Instance View 追蹤到分配內存較高/分配實例較多的實例類型, 跟蹤該實例類型的某幾個具體實例的 創建/分配 位置 (或者使用 CPU Profiler,跟蹤一段CPU執行的時間,如果發現某一段應用自有函數的調用代碼, 即 Call Chart 欄下的綠色條形在反覆地被執行,便是內存抖動的位置,追蹤這些綠色條形到重複執行的可疑函數的位置), 然後結合代碼進行排查,找到誘因位置。
本文參考
視頻:
國內Top團隊大牛帶你玩轉Android性能分析與優化
博客:
Android | App內存優化 之 內存抖動解決實戰
Android | App內存優化 之 全面理解MAT
Android | App內存優化 之 內存泄漏 要點概述 以及 解決實戰
Android | App內存優化 之 全面理解MAT
使用Android Studio和MAT進行內存泄漏分析
官網:
使用 Memory Profiler 查看 Java 堆和內存分配