Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android性能優化之被忽視的Memory Leaks

Android性能優化之被忽視的Memory Leaks

編輯:關於Android編程

起因

寫博客就像講故事,得有起因,經過,結果,人物,地點和時間。今天就容我給大家講一個故事。人物呢,肯定是我了。故事則發生在最近的這兩天,地點在coder君上班的公司。那天無意中我發現了一個奇怪的現象,隨著我點開我們App的頁面,Memory Monitor中顯示占用的內存越來越多(前面的頁面已經finish掉了)。咦?什麼鬼?

經過

有了問題就解決嘛,俗話說的好,有bug要上,沒有bug寫個bug也要上。那到底是是什麼問題會引起這個現象呢?

Android中內存相關的問題無非就是這麼幾點:

Memory Leaks 內存洩漏 Memory Churn 內存抖動 OutOfMemory 內存溢出

阿西吧,仔細想想怎麼這麼像內存洩漏呢。那到底是不是呢?那我們就一點一點分析一下呗。

內存相關數據

關於內存我們可能想了解的數據大概有三點:

總內存

private String getTotalMemory() {
    String str1 = "/proc/meminfo";// 系統內存信息文件
    String str2;
    String[] arrayOfString;
    long initial_memory = 0;
    try {
        FileReader localFileReader = new FileReader(str1);
        BufferedReader localBufferedReader = new BufferedReader(
                localFileReader, 8192);
        str2 = localBufferedReader.readLine();// 讀取meminfo第一行,系統總內存大小
        arrayOfString = str2.split("\\s+");
        for (String num : arrayOfString) {
            Log.i(str2, num + "\t");
        }
        initial_memory = Integer.valueOf(arrayOfString[1]).intValue() * 1024;// 獲得系統總內存,單位是KB,乘以1024轉換為Byte
        localBufferedReader.close();
    } catch (IOException e) {
    }
    return Formatter.formatFileSize(getBaseContext(), initial_memory);// Byte轉換為KB或者MB,內存大小規格化
}

系統當前可用內存

private String getAvailMemory() {
    // 獲取android當前可用內存大小
    ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
    am.getMemoryInfo(mi);
    //mi.availMem; 當前系統的可用內存
    return Formatter.formatFileSize(getBaseContext(), mi.availMem);// 將獲取的內存大小規格化
}

我們可以使用的內存

每一個Android設備都會有不同的RAM總大小與可用空間,因此不同設備為app提供了不同大小的heap限制。你可以通過調用getMemoryClass())來獲取你的app的可用heap大小。如果你的app嘗試申請更多的內存,會出現OutOfMemory的錯誤。

在一些特殊的情景下,你可以通過在manifest的application標簽下添加largeHeap=true的屬性來聲明一個更大的heap空間。如果你這樣做,你可以通過getLargeMemoryClass())來獲取到一個更大的heap size。

然而,能夠獲取更大heap的設計本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用大量的內存而去請求一個大的heap size。只有當你清楚的知道哪裡會使用大量的內存並且為什麼這些內存必須被保留時才去使用large heap. 因此請盡量少使用large heap。使用額外的內存會影響系統整體的用戶體驗,並且會使得GC的每次運行時間更長。在任務切換時,系統的性能會變得大打折扣。

另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。

private String getAllocationMemory() {
    // 獲取系統分配的內存大小
    ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    //開啟了android:largeHeap="true",米4系統能分配的內存為512M,不開啟為128M
    //return  am.getLargeMemoryClass()+"";
    //return  am.getMemoryClass()+"";
}       

Java中的四種引用

開始分析之前,有必要先了解下Java的內存分配與回收。

Java的數據類型分為兩類:基本數據類型、引用數據類型。

基本數據類型的值存儲在棧內存中,而引用數據類型需要開辟兩塊存儲空間,一塊在堆內存中,用於存儲該類型的對象;另一塊在棧內存中,用於存儲堆內存中該對象的引用。

其中引用類型變量分為四類:

強引用

最常用的引用形式。把一個對象賦給一個引用類型變量,則為強引用。

只要一個引用是強引用,則垃圾回收器永遠都無法回收這個對象的內存空間,除非JVM終止。

軟引用

當內存資源充足的時候,垃圾回收器不會回收軟引用對應的對象的內存空間;但當內存資源緊張時,軟引用所對應的對象就會被垃圾回收器回收。

//創建一個Student類型的軟引用

SoftReference sr = new SoftReference(new Student());

弱引用

不管JVM內存資源是否緊張,只要垃圾回收器運行,弱引用所對應的對象就會被釋放。

虛引用

虛引用等於沒有引用,無法通過虛引用訪問其對應的對象。

軟引用和弱引用在其對象被回收之後,這些引用會被添加到引用隊列中去;而虛引用在其對象被回收之前,虛引用就被添加到引用隊列中去了。因此虛引用可以在其對象被釋放之前進行一些操作。

虛引用和引用隊列綁定的方法:

//創建引用隊列  
ReferenceQueue queue = new ReferenceQueue();  
//創建虛引用,並綁定引用隊列  
PhantomReference str = new PhantomReference("啦啦啦",queue);   

Garbage Collection Android中的垃圾回收

Android系統會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的對象進行回收

執行GC操作的時候,所有線程的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續運行。

通常來說,單個的GC並不會占用太多時間,但是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了

Memory Leaks內存洩漏

內存洩漏表示的是不再用到的對象因為被錯誤引用而無法進行回收。發生內存洩漏會導致Memory Generation中的剩余可用Heap Size越來越小,這樣會導致頻繁觸發GC,更進一步引起性能問題。

總結起來其實很簡單:存在無效的引用!

內存洩露可以引發很多的問題,常見的內存洩露導致問題如下:

應用卡頓,響應速度慢(內存占用高時JVM虛擬機會頻繁觸發GC);

應用被從後台進程干為空進程;

應用莫名的崩潰(也就是超過了HeepSize阈值引起OOM);

內存洩漏分析工具

看到這些問題,突然發現好像離真相越來越近了0.0。

想要更加清楚地實時知曉當前應用程序的內存使用情況,我們需要通過一些工具來實現。比較好用的工具有兩種:

Memory Analyzer Tool LeakCanary

下面我們分開介紹。

Memory Analyzer Tool

Memory Analysis Tools(點我下載)是一個專門分析Java堆數據內存引用的工具,我們可以使用它方便的定位內存洩露原因,核心任務就是找到GC ROOT位置。接下來說下使用步驟。

抓取內存信息

AndriodStudio中抓取內存信息還是很方便的,有兩種方法:

使用Android Device Monitor

點擊Android Studio工具欄上的Tool–>Android Device Monitor

\

在Android Device Monitor界面中選在你要分析的應用程序的包名,點擊Update Heap來更新統計信息,然後點擊Cause GC即可查看當前堆的使用情況,點擊Dump HPROF file,將該應用當前的內存信息保存成hprof文件,放在桌面即可,操作如下圖

直接獲取<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPkFuZHJvaWQgU3R1ZGlvtcTX7tDCsOaxvr/J0tTWsb3Tu/HIoWhwcm9mzsS8/qOstavKx9ei0uI8c3Ryb25nPtTayrnTw9aux7DSu7ao0qrK1ravteO79yBJbml0aWF0ZSBHQ7C0xaXK1ravtKW3okdDPC9zdHJvbmc+o6zV4tH516W1vbXExNq05sq508PH6b/2vs3Kx7K7sPzAqFVucmVhY2hhYmxlttTP87XEoaM8L3A+DQo8cD48aW1nIGFsdD0="" src="/uploadfile/Collfiles/20160420/20160420091654326.png" title="\" />

稍等片刻,生成的文件會出現在captures中,然後選擇文件,點擊右鍵轉換成標准的hprof文件,就可以在MAT中打開了。

使用MAT工具查看分析

這裡我寫了個簡單的demo來測試,這個demo一共有兩個頁面,在跳轉到第二個頁面之後,新開一個現成去打印activity信息。

/**
 * 打印ActivityName
 */

public void printActivityName() {

    for (int i = 0; i < 100; i++) {

        new Thread(new Runnable() {

            @Override

            public void run() {

                while (true)

                    try {

                        Thread.sleep(1000 * 30);

                        Log.e(ActivityHelper.class.getSimpleName(), ((Activity) mContext).getClass().getSimpleName());

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

            }

        }).start();

    }

}   

多次進入SecondActivity之後會發現內存一直在增長,並沒有降低。

而且log裡會不停的輸出log,打印當前activity的name。

\

在MAT中打開抓取到的文件後如圖

MAT中提供了非常多的功能,這裡我們只要學習幾個最常用的就可以了。上圖最中央的那個餅狀圖展示了最大的幾個對象所占內存的比例,這張圖中提供的內容並不多,我們可以忽略它。紅色框中有兩個非常有用的工具是我們常用的。

Histogram可以列出內存中每個對象的名字、數量以及大小。

Dominator Tree會將所有內存中的對象按大小進行排序,並且我們可以分析對象之間的引用結構。

我們先來看Histogram

我們應該如何去分析內存洩漏呢?即分析大內存的對象。但是假如我們有目標對象的話,左上角值支持正則表達式的,我們輸入SecondActivity。這裡我們看到,我們有5個SecondActivity的實例,因為我們引用SecondActivity的現成沒有銷毀,導致會有很多實例。

接下來對著SecondActivity右鍵 -> List objects -> with incoming references查看具體SecondActivity實例,如下圖所示:

如果想要查看內存洩漏的具體原因,可以對著任意一個MainActivity的實例右鍵 -> Path to GC Roots -> exclude weak references,結果如下圖所示:

可以看到紅色框中,因為我們的線程持有SecondActivity的實例,所有導致內存洩漏。

此外,我們可以選擇以我們項目的包結構的形式來查看

接下來我們看下Dominator Tree。

關於Dominator Tree我們需要注意三點:

首先Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所占的總內存,因此從上圖中看,前兩行的Retained Heap是最大的,我們分析內存洩漏時,內存最大的對象也是最應該去懷疑的。帶有黃點的對象就表示是可以被GC Roots訪問到的,根據上面的講解,可以被GC Root訪問到的對象都是無法被回收的。並不是所有帶黃點的對象都是洩漏的對象,有些對象系統需要一直使用,本來就不應該被回收。我們可以注意到,有些帶黃點的對象最右邊會寫一個System Class,說明這是一個由系統管理的對象,並不是由我們自己創建並導致內存洩漏的對象。

現在我們可以對著我們想查看的內容點擊右鍵 -> Path to GC Roots -> exclude weak references,為什麼選擇exclude weak references呢?因為弱引用是不會阻止對象被垃圾回收器回收的,所以我們這裡直接把它排除掉,然後一步一步分析。

LeakCanary

leakcanary是一個開源項目,一個內存洩露自動檢測工具,是著名的GitHub開源組織Square貢獻的,它的主要優勢就在於自動化過早的發覺內存洩露、配置簡單、抓取貼心,缺點在於還存在一些bug,不過正常使用百分之九十情況是OK的,其核心原理與MAT工具類似。

因為配置十分簡單,這裡就不多說了,官方文檔。

我們看下分析結果

\

簡單直白!

常見內存洩漏情況

構造Adapter時,沒有使用緩存的 convertView

Bitmap對象不在使用時調用recycle()釋放內存

Context使用不當造成內存洩露:不要對一個Activity Context保持長生命周期的引用。盡量在一切可以使用應用ApplicationContext代替Context的地方進行替換。

非靜態內部類的靜態實例容易造成內存洩漏:即一個類中如果你不能夠控制它其中內部類的生命周期(譬如Activity中的一些特殊Handler等),則盡量使用靜態類和弱引用來處理(譬如ViewRoot的實現)。

警惕線程未終止造成的內存洩露;譬如在Activity中關聯了一個生命周期超過Activity的Thread,在退出Activity時切記結束線程。一個典型的例子就是HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命周期超過了Activity生命周期,我們必須手動在Activity的銷毀方法中中調運thread.getLooper().quit();才不會洩露。

對象的注冊與反注冊沒有成對出現造成的內存洩露;譬如注冊廣播接收器、注冊觀察者(典型的譬如數據庫的監聽)等。

創建與關閉沒有成對出現造成的洩露;譬如Cursor資源必須手動關閉,WebView必須手動銷毀,流等對象必須手動關閉等。

不要在執行頻率很高的方法或者循環中創建對象(比如onmeasure),可以使用HashTable等創建一組對象容器從容器中取那些對象,而不用每次new與釋放。

避免代碼設計模式的錯誤造成內存洩露;譬如循環引用,A持有B,B持有C,C持有A,這樣的設計誰都得不到釋放。

結果

真相只有一個,那就是確實是由於內存洩漏才出現我遇到的情況。程序員嘛,誰還不踩個坑,跳出來,拍拍身上的灰塵,總結一下,過兩天又是一條幫幫的coder。源碼

   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved