起源
有趣的靈魂千奇百怪,記憶體洩漏的也是各式各樣 我在15年寫過一遍 文章 《 android中常見的記憶體洩漏和解決辦法》 http://blog.csdn.net/wanghao200906/article/details/50426881 ,時隔三年居然還有人我問 該如何解決 記憶體洩漏的問題,因為 有趣的靈魂 千奇百怪,所以 記憶體洩漏的也是各式各樣,所以想避免 記憶體洩漏 ,不能只記住 常見 問題的程式碼,而是要學會如果發現記憶體洩漏的方法。
學習內容
- 記憶體洩漏的一些 基礎支援(估計有你不會的)
- 學會使用android studio 3.0 自帶的 android profile 檢查記憶體洩漏
- 使用 mat 工具 來檢查 記憶體洩漏
記憶體洩漏的基礎
記憶體洩漏:記憶體不在GC掌控之內了。當一個物件已經不需要再使用了,本該被回收時,而有另外一個正在使用的物件持有它的引用從而就導致,物件不能被回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏
四中引用:
- StrongReference強引用: 回收時機:從不回收 使用:物件的一般儲存 生命週期:JVM停止的時候才會終止
- SoftReference軟引用 回收時機:當記憶體不足的時候;使用:SoftReference結合- ReferenceQueue構造有效期短;生命週期:記憶體不足時終止
- WeakReference,弱引用 回收時機:在垃圾回收的時候;使用:同軟引用; 生命週期:GC後終止
- PhatomReference 虛引用 回收時機:在垃圾回收的時候;使用:合ReferenceQueue來跟蹤物件唄垃圾回收期回收的活動; 生命週期:GC後終止
開發時,為了防止記憶體溢位,處理一些比較佔用記憶體大並且生命週期長的物件的時候,可以儘量使用軟引用和弱引用。 軟引用比LRU演算法更加任性,回收量是比較大的,你無法控制回收哪些物件。
因為我們主要是 查詢 記憶體洩漏 ,這裡不對 基礎知識 做過多的擴充
記憶體分配
- 成員變數全部儲存在堆中(包括基本資料型別,引用及引用的物件實體)---因為他們屬於類,類物件最終還是要被new出來的。
- 區域性變數的基本資料型別和引用儲存於棧當中,引用的物件實體儲存在堆中。-----因為他們屬於方法當中的變數,生命週期會隨著方法一起結束。 看下面程式碼
public class Main{
int a = 1; // a 和1 都在堆裡
Student s = new Student();// s 和new d的Student()都在 堆裡
public void XXX(){
int b = 1;//b 和 1 棧裡面
Student s2 = new Student();// s2 在棧裡, new的 Student() 在堆裡
}
}
ok 如果你不同意,在你查詢完資料之後, 歡迎來討論
複製程式碼
基礎知識就到這吧。下面開始主題
Android Profiler 查詢記憶體洩漏
該方法可以解決一部分問題,但不是很好用,之所以得學習他是因為為後面做個鋪墊
下面來介紹一下,因為不是特別好用 所以用一個簡單的例子介紹一下, 看下面的程式碼
package sven.com.practise32_performance_optimization;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CommonUtils.getInstance(this);
}
}
package sven.com.practise32_performance_optimization;
import android.content.Context;
/**
* Created by wanghao on 2018/2/8.
*/
public class CommonUtils {
private static CommonUtils instance;
private Context context;
private CommonUtils(Context context) {
this.context = context;
}
public static CommonUtils getInstance(Context context) {
if (instance == null) {
instance = new CommonUtils(context);
}
return instance;
}
}
複製程式碼
我們看一下 android profiler
這樣的。然後我們點選中間的MEMORY.
然後我們執行如下操作。 我們 橫屏 , 然後在豎屏
- 橫屏一下 就會將該MainActivity onDestory掉。然後在onCreate();
橫屏豎屏,操作結束後我們點選 箭頭指向的按鈕,如下圖
我們看到最下面
這就是 堆的一些資訊。
一些資訊的含義
-
Heap Count:堆中的例項數。
-
Shallow Size:此堆中所有例項的總大小(以位元組為單位)。
-
Retained Size:為此類的所有例項而保留的記憶體總大小(以位元組為單位)。 在類列表頂部,您可以使用左側下拉選單在以下堆轉儲之間進行切換:
-
Default heap:系統未指定堆時。
-
App heap:您的應用在其中分配記憶體的主堆。
-
Image heap:系統啟動映像,包含啟動期間預載入的類。 此處的分配保證絕不會移動或消失。
-
Zygote heap:寫時複製堆,其中的應用程式是從 Android 系統中派生的。 預設情況下,此堆中的物件列表按類名稱排列。 您可以使用其他下拉選單在以下排列方式之間進行切換:
-
Arrange by class:基於類名稱對所有分配進行分組。
-
Arrange by package:基於軟體包名稱對所有分配進行分組。
-
Arrange by callstack:將所有分配分組到其對應的呼叫堆疊。 此選項僅在記錄分配期間捕獲堆轉儲時才有效。 即使如此,堆中的物件也很可能是在您開始記錄之前分配的,因此這些分配會首先顯示,且只按類名稱列出。 預設情況下,此列表按 Retained Size 列排序。 您可以點選任意列標題以更改列表的排序方式。
在 Instance View 中,每個例項都包含以下資訊:
- Depth:從任意 GC 根到所選例項的最短 hop 數。
- Shallow Size:此例項的大小。
- Retained Size:此例項支配的記憶體大小(根據 dominator 樹)。
接下來我們檢視app Heap 中的內容 經過漫長的查詢。我們找到了MainActivity (這就是我覺得 android profiler 不要用的地方,必須得挨個查詢,效率太低了)。
點選MainActivity. 然後右邊出來了 Instance View ,裡面出現了 三個 MainActivity 。
分析1
-
我們看到Depth 從上到下 是 2 ,0,3 ,上文說了 Depth:從任意 GC 根到所選例項的最短 hop 數。
-
在回憶剛才,我們 開啟app,橫屏,豎屏,建立了3個MainAvtivity ,這點兒可以理解。
-
2,0,3 怎麼解釋呢。 0 就代表該activity,可以被 gc 回收 2 ,3 就代表 最短的hop數不為零。肯定不會被gc回收。
-
我們點選第一個MainAvtivity看看
我們看到 instance 這個程式碼, 是我們自己定義的,它持有了MainActivity 。
- 在看第三個MainAvtivity
我們看到這裡就沒有剛才 被CommonUtils中 instance 持有的activity。說明 當前的activity。是我們看到的MainActivity。他當然不可能被銷燬。同時因為 第一次建立的MainActivity的時候 生成了 CommonUtils中 instance 它不為null。所以 在建立MainActivity的時候 它的上下文不會再次被持有。
到這裡我們執行以下gc操作,然後在點選dump操作
在根據剛才的方法 找到MainAvtivity。點選。 結果如下
我們看到 這個 MainAvtivity被持有了。所以MainActivity 在橫屏的時候,雖然執行了onDestory(),但依然不會被銷燬,他無法被回收,所以就會記憶體洩露。
到此 android profiler 中 查詢記憶體洩漏的方法就介紹完了。
我覺得的缺點就是,如果程式碼過於複雜,我們又不知道查詢那些程式碼。只能挨個點選,挨個看,太耗時耗力了。
所以 還是MAT 工具是最好用的。
MAT 完美查詢記憶體洩漏
為了 說明MAT 的強大 我們寫一個稍微複雜的記憶體洩漏的程式碼
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static Link linkInstance;
class Link {
public void dosomething() {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (linkInstance == null) {
linkInstance = new Link();
}
CommonUtils.getInstance(this);
new MyThread().start();
}
class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
複製程式碼
然後執行才做: 執行 ,橫屏 ,豎屏,在android profile中 點選 gc,然後在點選dump java heap。效果如下
點選Export capture to file 儲存到本地資料夾。格式為 xxx.hprof 比如 叫 test1.hprof
然後執行命令列
hprof-conv /Users/mypro/Desktop/mat/test1.hprof /Users/mypro/Desktop/mat/test2.hprof
複製程式碼
- /Users/mypro/Desktop/mat/test1.hprof 是我們在as中儲存的hprof檔案
- /Users/mypro/Desktop/mat/test2.hprof 是我們用命令列要生成的 hprof檔案,為了mat可以識別
然後 使用MAT 工具了。可惜的是 as沒有Mat 工具。只能下載一個 Mat工具 http://www.eclipse.org/mat/downloads.php
下載完之後就安裝,開啟。然後直接把test2.hprof 檔案 拖到 Mat 工具中。
為了 以後的方面快捷,真的很有必要下載一個Mat工具,你是程式設計師,為了完美的找到記憶體洩漏的地方,難道還怕麻煩麼,難道你看到測試組 報告後 ,你的程式碼有好多記憶體洩漏的地方,你說你面子往哪擱。
在選擇框中選擇第一個,生成報告。
然後點選 histogram,效果如下
我們看到了 有好多的類。跟在android profile中看到的一樣。但是這麼多。我們不用挨個檢查,只要檢查我們自己寫的程式碼即可,如果在最上面可以輸入包名,然後回車。
可以看到 出現的都是我們自己寫的程式碼了。進行挨個分析
分析
選擇第一個右鍵,List objects -> with incoming references ->回車
- with incoming references : 表示該類被 哪些外部對應 引用了
- with outgoing references : 表示 該類 持有了 哪些外部物件的引用
回車 之後我們發現有三個 類如下圖。
我們發現 第一個MainActivity 被 好多 物件引用著了。該如挨個查詢呢。莫慌。如下圖操作
我們選擇 去掉 弱引用,軟引用 所 引用的物件。
可以看到 我們的MainAvtivity類,被 一個 MyThread的執行緒 所引用了。找到以一個 記憶體洩漏的地方。 因為我們 內部類會持有 外部類的引用。所以 MyThread 類持有了MainActiviy的 上下文。又因為 thread 是一個 while(true)的死迴圈,所以 不會釋放。
我們現在 依據上面 的分析流程,繼續檢視第二個類。
還是右鍵->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回車
ok,我們發現MainActivity 的上下文被好幾個類引用了。但是除了MyThread 類是我們自己定義的,其他的類都是 安卓原始碼,所以我們可以推斷,當前的MainActivity類可能是 正在展示在手機頁面上的 MainActivity 類。所以該類並不存在記憶體洩漏的問題。
我們看完第二個 再看看三個 。
還是右鍵->Path to Gc Root -> exclude all phantom/weak/soft etc. references- >回車
ok,我們看到 三個地方引用 了MainActivity 的 上下文。這三個地方度出現了 記憶體溢位。
- MainActivity 建立了三次,一共建立三次 MyThread 類,MyThread 類屬於內部類,他持有外部類的額引用
- CommonUtils 屬於 單利,當CommonUtils 的instance 不為null的時候 就不會再繼續持有 MainActivity的上下文引用。所以只持有了一次
- Link 屬於 內部類 持有MainActivity 的引用,但是在onCreate裡面有判斷只有當 Link的instance為null的時候才會建立一次。所以也只持有了 一次MainActivity的上下文引用。
到這裡 記憶體洩漏的方法就介紹完了。學會該方法,不管是什麼記憶體洩漏都可以輕鬆的找到了。 能找到記憶體洩漏的地方,基本上就可以解決了。
下面在稍微總結一下 常見記憶體洩漏的就該方法。。
- 靜態變數引起的記憶體洩露
當呼叫getInstance時,如果傳入的context是Activity的context。只要這個單利沒有被釋放,那麼這個
Activity也不會被釋放一直到程式退出才會釋放。
public class CommUtil {
private static CommUtil instance;
private Context context;
private CommUtil(Context context){
this.context = context;
}
public static CommUtil getInstance(Context mcontext){
if(instance == null){
instance = new CommUtil(mcontext);
}
// else{
// instance.setContext(mcontext);
// }
return instance;
}
複製程式碼
- 非靜態內部類引起記憶體洩露(包括匿名內部類)
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// private static Link linkInstance;
private Link linkInstance;
private boolean flag = true;
class Link {
public void dosomething() {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (linkInstance == null) {
linkInstance = new Link();
}
// CommonUtils.getInstance(this);
CommonUtils.getInstance(getApplicationContext());
new MyThread().start();
}
// 錯誤寫法
// class MyThread extends Thread {
// @Override
// public void run() {
// while (true) {
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
// }
// 解決方法 方法1
// class MyThread extends Thread {
// @Override
// public void run() {
// while (flag) { //MainActivity 銷燬的時候讓thread 停止
// try {
// sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// }
//
// 解決方法2 讓MyThread 為 靜態內部類,靜態內部類就不會持有 外部類的引用
//
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
flag = false;
}
}
複製程式碼
再來一個 handler的內部類 錯誤示範
//錯誤的示範:
//mHandler是匿名內部類的例項,會引用外部物件MainActivity.this。如果Handler在Activity退出的時候,它可能還活著,這時候就會一直持有Activity。
// private Handler mHandler = new Handler(){
// @Override
// public void handleMessage(Message msg) {
// super.handleMessage(msg);
// switch (msg.what){
// case 0:
// //載入資料
// break;
//
// }
// }
// };
//解決方案:
private static class MyHandler extends Handler{
// private MainActivity mainActivity;//直接持有了一個外部類的強引用,會記憶體洩露
private WeakReference<MainActivity> mainActivity;//設定軟引用儲存,當記憶體一發生GC的時候就會回收。
public MyHandler(MainActivity mainActivity) {
this.mainActivity = new WeakReference<MainActivity>(mainActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
MainActivity main = mainActivity.get();
if(main==null||main.isFinishing()){
return;
}
switch (msg.what){
case 0:
//載入資料
// MainActivity.this.a;//有時候確實會有這樣的需求:需要引用外部類的資源。怎麼辦?
int b = main.a;
break;
}
}
};
複製程式碼
- 不需要用的監聽未移除會發生記憶體洩露
例子1:
// tv.setOnClickListener();//監聽執行完回收物件
//add監聽,放到集合裡面
tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
@Override
public void onWindowFocusChanged(boolean b) {
//監聽view的載入,view載入出來的時候,計算他的寬高等。
//計算完後,一定要移除這個監聽
tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
}
});
例子2:
SensorManager sensorManager = getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this,sensor,SensorManager.SENSOR_DELAY_FASTEST);
//不需要用的時候記得移除監聽
sensorManager.unregisterListener(listener);
複製程式碼
- 資源未關閉引起的記憶體洩露情況
比如:BroadCastReceiver、Cursor、Bitmap、IO流、自定義屬性attribute
attr.recycle()回收。
當不需要使用的時候,要記得及時釋放資源。否則就會記憶體洩露。
複製程式碼
- 無限迴圈動畫 沒有在onDestroy中停止動畫,否則Activity就會變成洩露物件。 比如:輪播圖效果。