Android記憶體洩露分析以及工具的使用

鋸齒流沙發表於2017-12-22

Android效能優化對於開發者來說是一個不可小覷的問題,如果軟體的效能極差,造成介面卡頓,甚至直接掛掉,對於使用者來說是一個極其致命的,可能會導致使用者直接把應用給解除安裝了。相反的,如果把效能優化得極致,執行得很流暢,從而增加使用者的好感,得到好評,所以效能優化對於開發者來說是非常重要的。

Android的效能優化通常涉及到記憶體洩露檢測、渲染效能優化、電量優化、網路優化和Bitmap記憶體管理優化,以及多執行緒優化等等,當然效能優化的不止這些,除此之外還有安裝包優化和資料傳輸效率等,所以Android的效能優化涉及的範圍是比較廣的。心急吃不了熱豆腐,因此需要我們一點點來學習,慢慢研究。

記憶體洩露

記憶體洩露,關乎到開發者本身寫程式碼的問題,所以平時開發者寫程式碼要有嚴謹性和清晰的邏輯,申請的記憶體,沒用之後,就要釋放掉。那麼什麼是記憶體洩露呢?

瞭解記憶體洩露,首先需要了解java的記憶體分配,其中主要包括靜態儲存區、棧區、堆區、暫存器、常量池等。

靜態儲存區:記憶體在程式編譯的時候就已經分配好,這塊的記憶體在程式整個執行期間都一直存在,主要存放靜態資料、全域性的static資料和一些常量。

棧區:儲存區域性變數的值,其中包括:用來儲存基本資料型別的值、儲存類的例項(即堆區物件的引用(指標)),以及用來儲存載入方法時的幀。也就是說函式一些內部變數的儲存在棧區,函式執行結束的時,這些儲存單元就會自動被釋放掉。因為棧記憶體內建在處理器的裡面,所以運算速度很快,但是棧區的容量有限。

堆區:也叫做動態記憶體分配,用來存放動態產生的資料,如new出來的物件。用malloc或者new來申請分配一個記憶體。在C/C++可能需要自己負責釋放,但在java裡面直接依賴GC機制。

暫存器:JVM內部虛擬暫存器,存取速度非常快,程式不可控制。

常量池:存放常量。

關於記憶體分配,讀者可以參考《Java 記憶體分配全面淺析》

棧和堆的區別: 1)堆是不連續的記憶體區域,堆空間比較靈活也特別大。 2)棧式一塊連續的記憶體區域,大小是由作業系統決定的。

由於堆是不連續的記憶體區域,管理起來特別的麻煩,如果頻繁的new和remove,可能會造成大量的記憶體碎片,所造成的記憶體碎片就造成了記憶體洩露,這樣就會導致執行效率低下。但是對於棧,棧的特點是先進後出,進出完全不會產生碎片,執行效率高且穩定。因此我們關於記憶體洩露,主要看堆記憶體。

記憶體洩露(memory leak):是指程式在申請記憶體後,無法釋放已申請的記憶體空間。也就是說當一個物件已經不再使用了,本該被回收時,但有另外一個正在使用的物件持有它的引用從而就導致物件不能被回收。這種導致了本該被回收的物件不能被回收而停留在堆記憶體中,就產生了記憶體洩漏。

記憶體溢位

記憶體溢位(out of memory):是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory。在Android中如果出現記憶體溢位,也就是我們經常看到的OOM情況,那麼應用就會閃退。由於記憶體洩露,而導致可用的記憶體空間越來越少,從而導致OOM。因此平時寫程式碼特別需要主要記憶體洩露的情況,因為一旦出現記憶體洩露,隨著洩露的記憶體越來越多,就會造成記憶體溢位。

既然是記憶體洩露會導致記憶體溢位,歸根結底還是需要優化記憶體洩露。優化記憶體洩露,首先需要找到記憶體洩露的地方,然後才能去優化。

確定記憶體洩露

1)使用AndroidStudio自帶的Memory Monitors進行記憶體分析。

monitor

通過視覺化可以觀察到該應用的Memery、CPU、NetWork和GPU變化等情況。這裡我們主要觀察Memery(記憶體)即可,上圖是我開啟應用的首頁時Memery的情況。

然後點選幾次InitiateGC:

monitor

這是點選GC之後,穩定下來的情況,基本上時一條水平線的狀態的了。

monitor

Free:表示還可用的記憶體,在圖中淺灰色表示。 Allocated:表示已經分配的記憶體大小,同樣在圖中藍色表示

當我進入下個頁面的時候,明顯看到記憶體變化

monitor

當我返回上一個頁面的時候,然後GC

monitor

上圖就是返回之後,點選GC的情況,Free和Allocated並沒有變化,說明剛剛進入的那個頁面就沒有出現記憶體洩露的情況,如果出現變化比較明顯,那就可以判斷剛剛所進入的頁面出現了記憶體洩露的情況。同樣也可以使用Heap Viewer觀察記憶體洩露。

Heap Viewer

Heap Viewer:能夠實時檢視App分配的記憶體大小和空閒記憶體大小,並發現記憶體洩露。除此功能以外,Heap Viewer還可以檢測記憶體抖動,因為記憶體抖動的時候,會頻繁發生GC,這個時候我們只需要開啟Heap Viewer,觀察資料的變化,如果發生記憶體抖動,會觀察到資料在短時間內頻繁更新。

啟動Heap Viewer:

monitor

選擇裝置下對應的包名,然後update Heap

monitor

選擇Head,然後GC

monitor

monitor

點選Cause GC,發現所有的資料都更新了,更新後的表格顯示,在Heap上哪些資料是可用的,選中其中任一行資料,就可以看到詳細資料。

data object的total size就是當前程式中Java的物件所佔用的記憶體總量。我們反覆執行某一個操作並同時執行GC排除可以回收掉的記憶體,注意觀察data object的Total Size值,正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程式中的的程式碼良好,沒有造成物件不被垃圾回收的情況。反之如果程式碼中存在沒有釋放物件引用的情況,隨著操作次數的增多Total Size的值會越來越大。

點選class object,螢幕上馬上出現大量更新的資料,矩形圖列出這一資料記憶體分配的數量,以及確切的容量,heap viewer可以有效地分析程式在堆中所分配的資料型別,以及數量和大小。

Allocation Tracker

除了Head Viewer和Memory Monitor,還可以使用Allocation Tracker(分配追蹤器)。

monitor

關於Allocation Tracker可以檢視 《Android效能專項測試之Allocation Tracker(Android Studio)》 這篇文章進行學習。

以上的工具都具有不同的特點,具體使用那一個工具可以按照以下來劃分: 1)Memory Monitor:獲得記憶體的動態檢視 2)Heap Viewer:顯示堆記憶體中儲存了什麼 3)Allocation Tracker:具體是哪些程式碼使用了記憶體

使用MAT記憶體分析工具

MAT:Memory Analyzer Tools,一款詳細分析Java堆記憶體的工具,從而能夠分析出記憶體洩露的詳細情況。

使用AndroidStudio生成hprof檔案:

monitor

生成的hprof檔案不能直接交給MAT, MAT是不識別的, 我們需要右鍵點選這個檔案,轉換成MAT識別的。

monitor

在eclipse中安裝MAT,然後開啟hprof檔案:

monitor

monitor

使用MAT來分析記憶體洩露的情況: 1、根據data object的Total Size,找到記憶體洩露的操作; 2、找到記憶體洩露的物件(懷疑物件),也就是通過MAT對比操作前後的hprof檔案來定位記憶體洩露,是那個資料物件記憶體洩露了; 3、找到記憶體洩露的原因,也就是那個物件持有了第2個步驟找出來的發生記憶體洩露的物件。

具體步驟: 1)進入Histogram,過濾出某一個嫌疑物件類;

monitor

2)分析持有此類物件引用的外部物件;

monitor

3)過濾掉一些弱引用、軟引用、虛引用,因為它們遲早可以被GC幹掉不屬於記憶體洩露。

monitor

過濾掉之後就需要進入程式碼分析此時的物件的引用持有是否合理,然後進行解決。

記憶體優化一般分為兩方面,一方面在開發過程中避免寫出有記憶體洩露的程式碼,另一方面就是我們前面介紹的,利用一些記憶體分析工具檢測出潛在的記憶體洩露。我看看平時我們開發中,那些需要注意記憶體洩露的地方。

靜態變數引起的記憶體洩露
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        sContext = this;
    }
複製程式碼

以上程式碼Activity無法正常銷燬,因為sContext引用了它。

單例模式所造成的記憶體洩露
public class MyInstance {

    private static MyInstance instance;
    private Context context;

    private MyInstance(Context context) {
        this.context = context;
    }

    public static MyInstance getInstance(Context mcontext) {
        if (instance == null) {
            instance = new MyInstance(mcontext);
        }
        return instance;
    }

    public void setContext(Context context) {
        this.context = context;
    }
}
複製程式碼

單例模式的生命週期和Application保持一致,如果在Activity中呼叫getInstance,把Activity的Context傳入,那麼Activity的物件就會被單例模式的MyInstance所持有,造成記憶體洩露,其實也是同屬於靜態變數引起的記憶體洩露,因為instance就是靜態變數。而靜態變數屬於靜態儲存方式,其儲存空間為記憶體中的靜態資料區(在靜態儲存區內分配儲存單元),該區域中的資料在整個程式的執行期間一直佔用這些儲存空間(在程式整個執行期間都不釋放)。

非靜態內部類引起記憶體洩露
    //隱式持有Activity例項,Activity.this.a
    public void loadData(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    try {
                        int b=a;
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
複製程式碼

還有Handler使用非靜態內部類的形式:

     private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
複製程式碼

如何解決非靜態內部類引起記憶體洩露的問題呢?這就需要將靜態內部類修改為靜態內部類,因為靜態內部類不會隱式持有外部類。

//解決方案:
    private static class MyHandler extends Handler{
        //設定軟引用儲存,當記憶體一發生GC的時候就會回收。
        private WeakReference<MainActivity> mainActivity;

        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;

            }
        }
    };
複製程式碼

在上面的程式碼中使用靜態內部類的形式建立了一個繼承Handler的MyHandler,並且內部使用弱引用WeakReference,既WeakReference,如果不用弱引用的話,mainActivity就會直接持有了一個外部類的強引用,導致記憶體洩露。最好在onDestroy呼叫Handler的removeCallbacksAndMessages方法。

    @Override
    protected void onDestroy() {
        super.onDestroy();
        MyHandler.removeCallbacksAndMessages(null);
    }
複製程式碼

上面提到了弱引用,這裡我們需要了解引用相關的知識:

1)StrongReference(強引用):從不回收,在JVM停止的時候才會終止。

2)SoftReference(軟引用):當記憶體不足的時候就會回收。

3)WeakReference(弱引用):在垃圾回收的時候就會回收,它在GC後終止。

4)PhatomReference(虛引用):在垃圾回收的時候就會回收,同樣在GC後終止。

資源未關閉引起的記憶體洩露情況

平時用到的資源,用完之後需要關閉,防止記憶體洩露,如BroadCastReceiver、Cursor、Bitmap、IO流和自定義屬性AttributeSet等資源。在自定義的AttributeSet資源用完之後,需要呼叫attrs.recycle()進行回收。否則會造成記憶體洩露。

屬性動畫導致的記憶體洩露

屬性動畫有一類無限迴圈動畫,如果沒有在onDestroy方法中停止動畫,Activity就會導致記憶體洩露。因為,如果沒有停掉動畫的話,Activity的View就會被動畫持有,而View又持有了Activity,最終Activity無法釋放。

用完後的監聽未移除導致記憶體洩露
public class ListenerCollector {

    static private WeakHashMap<View, MyView.MyListener> sListener = new WeakHashMap<>();

    public void setsListener(View view, MyView.MyListener listener) {
        sListener.put(view, listener);
    }

    public static void clearListeners() {
        //移除所有監聽。
        sListener.clear();
    }

}
複製程式碼

如上面的程式碼,新增了監聽,使用完之後,需要在onDestroy方法裡需要呼叫clearListeners方法,移除監聽。關於WeakHashMap的特點就是當除了自身有對key的引用外,如果此key沒有其他引用那麼此map會自動丟棄此值,如上面的view=null,那麼sListener裡面的view就會被丟棄。

相關文章