Android 效能優化 - 徹底解決記憶體洩漏

svenWang_發表於2018-02-10

起源

有趣的靈魂千奇百怪,記憶體洩漏的也是各式各樣 我在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就會變成洩露物件。 比如:輪播圖效果。

效能優化系列的 記憶體洩漏到此就結束了希望該部落格能幫助大家解決記憶體洩漏的問題。

相關文章