Android最佳性能實踐(二)——分析內存的使用狀況

轉載自:http://blog.csdn.net/guolin_blog/article/details/42238633php

因爲Android是爲移動設備開發的操做系統,咱們在開發應用程序的時候應當始終把內存問題充分考慮在內。雖然Android系統擁有垃圾自動回收機制,但這並不意味着咱們就能夠徹底忽略什麼時候去分配或釋放內存。即便咱們所有按照上一篇文章中給出的編程建議來去編寫程序,仍是會頗有可能出現內存泄露或其它類型的內存問題。因此,惟一可以解決問題的辦法,就是嘗試去分析應用程序的內存使用狀況,那麼本篇文章就會教你們如何進行分析。若是你尚未看過前面一篇文章,建議先去閱讀 Android最佳性能實踐(一)——合理管理內存java

雖然說如今的手機內存都已經很是大了,可是咱們你們都知道,系統是不可能將全部的內存都分配給咱們的應用程序的。沒錯,每一個程序都會有可以使用的內存上限,這被稱爲堆大小(Heap Size)。不一樣的手機,堆大小也不盡相同,隨着如今硬件設備不斷提升,堆大小也已經由Nexus One時的32MB,變成了Nexus 5時的192MB。若是你們想要知道本身手機的堆大小是多少,能夠調用以下代碼:正則表達式

  1. ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE); 
  2. int heapSize = manager.getMemoryClass(); 
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();

結果是以MB爲單位進行返回的,咱們在開發應用程序時所使用的內存不能超出這個限制,不然就會出現OutOfMemoryError。所以,好比說咱們的程序中須要緩存一些數據,就能夠根據堆大小來決定緩存數據的容量。編程

下面咱們來討論一下Android的GC操做,GC全稱是Garbage Collection,也就是所謂的垃圾回收。Android系統會在適當的時機觸發GC操做,一旦進行GC操做,就會將一些再也不使用的對象進行回收。那麼哪些對象會被認爲是再也不使用,而且能夠被回收的呢?咱們來看下面一張圖:緩存

上圖當中,每一個藍色的圓圈就表明一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關係。這些對象有些是處於活動狀態的,而有些就已經再也不被使用了。那麼GC操做會從一個叫做Roots的對象開始檢查,全部它能夠訪問到的對象就說明還在使用當中,應該進行保留,而其它的對象就表示已經再也不被使用了,以下圖所示:併發

能夠看到,目前全部黃色的對象仍然會被系統繼續保留,而藍色的對象就會在GC操做當中被系統回收掉了,這大概就是Android系統一次簡單的GC流程。eclipse

那麼何時會觸發GC操做呢?這個一般都是由系統去決定的,咱們通常狀況下都不須要主動通知系統應該去GC了(雖然咱們確實能夠這麼作,下面會講到),可是咱們仍然能夠去監聽系統的GC過程,以此來分析咱們應用程序當前的內存狀態。那麼怎樣才能去監聽系統的GC過程呢?其實很是簡單,系統每進行一次GC操做時,都會在LogCat中打印一條日誌,咱們只要去分析這條日誌就能夠了,日誌的基本格式以下所示:ide

  1. D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time> 
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>

注意這裏我仍然是以dalvik虛擬機來進行說明,art狀況下打印的內容也是基本相似的。。工具

 

首先第一部分GC_Reason,這個是觸發此次GC操做的緣由,通常狀況下一共有如下幾種觸發GC操做的緣由:性能

  • GC_CONCURRENT:   當咱們應用程序的堆內存快要滿的時候,系統會自動觸發GC操做來釋放內存。
  • GC_FOR_MALLOC:   當咱們的應用程序須要分配更多內存,但是現有內存已經不足的時候,系統會進行GC操做來釋放內存。
  • GC_HPROF_DUMP_HEAP:   當生成HPROF文件的時候,系統會進行GC操做,關於HPROF文件咱們下面會講到。
  • GC_EXPLICIT:   這種狀況就是咱們剛纔提到過的,主動通知系統去進行GC操做,好比調用System.gc()方法來通知系統。或者在DDMS中,經過工具按鈕也是能夠顯式地告訴系統進行GC操做的。

接下來第二部分Amount_freed,表示系統經過此次GC操做釋放了多少內存。

而後Heap_stats中會顯示當前內存的空閒比例以及使用狀況(活動對象所佔內存 / 當前程序總內存)。

最後Pause_time表示此次GC操做致使應用程序暫停的時間。關於這個暫停的時間,Android在2.3的版本當中進行過一次優化,在2.3以前GC操做是不能併發進行的,也就是系統正在進行GC,那麼應用程序就只能阻塞住等待GC結束。雖然說這個阻塞的過程並不會很長,也就是幾百毫秒,可是用戶在使用咱們的程序時仍是有可能會感受到略微的卡頓。而自2.3以後,GC操做改爲了併發的方式進行,就是說GC的過程當中不會影響到應用程序的正常運行,可是在GC操做的開始和結束的時候會短暫阻塞一段時間,不過優化到這種程度,用戶已是徹底沒法察覺到了。

下面是一次GC操做在LogCat中打印的日誌:

能夠看出,和咱們上面所介紹的格式是徹底一致的,最後的暫停時間31ms+7ms,一次就是GC開始時的暫停時間,一次是結束時的暫停時間。另外能夠根據進程id來區分這是哪一個程序中進行的GC操做,那麼從上圖就能夠看出這條GC日誌是屬於24699這個程序的。

那麼這是使用dalvik運行環境時所打印的GC日誌,而自Android 4.4版本以後加入了art運行環境,在art中打印GC日誌基本和dalvik是相同的,以下圖所示:

相信沒有什麼難理解的地方吧,art中只是內容顯示的格式有了稍許變化,打印的主體內容仍然是不變的。

好的,經過日誌的方式咱們能夠簡單瞭解到系統的GC工做狀況,可是若是咱們想要更加清楚地實時知曉當前應用程序的內存使用狀況,只經過日誌就有些力不從心了,咱們須要經過DDMS中提供的工具來實現。

打開DDMS界面,在左側面板中選擇你要觀察的應用程序進程,而後點擊Update Heap按鈕,接着在右側面板中點擊Heap標籤,以後不停地點擊Cause GC按鈕來實時地觀察應用程序內存的使用狀況便可,以下圖所示:

接着繼續操做咱們的應用程序,而後繼續點擊Cause GC按鈕,若是你發現反覆操做某一功能會致使應用程序內存持續增高而不會降低的話,那麼就說明這裏頗有可能發生內存泄漏了。

好了,討論完了GC,接下來咱們討論一下Android中內存泄漏的問題。你們須要知道的是,Android中的垃圾回收機制並不能防止內存泄漏的出現,致使內存泄漏最主要的緣由就是某些長存對象持有了一些其它應該被回收的對象的引用,致使垃圾回收器沒法去回收掉這些對象,那也就出現內存泄漏了。好比說像Activity這樣的系統組件,它又會包含不少的控件甚至是圖片,若是它沒法被垃圾回收器回收掉的話,那就算是比較嚴重的內存泄漏狀況了。

下面咱們來模擬一種Activity內存泄漏的場景,內部類相信你們都有用過,若是咱們在一個類中又定義了一個非靜態的內部類,那麼這個內部類就會持有外部類的引用,以下所示:

  1. public class MainActivity extends ActionBarActivity { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         setContentView(R.layout.activity_main); 
  7.         LeakClass leakClass = new LeakClass(); 
  8.     } 
  9.  
  10.     class LeakClass { 
  11.  
  12.     } 
  13.     ...... 
public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakClass leakClass = new LeakClass();
    }

    class LeakClass {

    }
    ......
}

目前來看,代碼仍是沒有問題的,由於雖然LeakClass這個內部類持有MainActivity的引用,可是隻要它的存活時間不會長於MainActivity,就不會阻止MainActivity被垃圾回收器回收。那麼如今咱們來將代碼進行以下修改:

  1. public class MainActivity extends ActionBarActivity { 
  2.  
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         super.onCreate(savedInstanceState); 
  6.         setContentView(R.layout.activity_main); 
  7.         LeakClass leakClass = new LeakClass(); 
  8.         leakClass.start(); 
  9.     } 
  10.  
  11.     class LeakClass extends Thread { 
  12.  
  13.         @Override 
  14.         public void run() { 
  15.             while (true) { 
  16.                 try { 
  17.                     Thread.sleep(60 * 60 * 1000); 
  18.                 } catch (InterruptedException e) { 
  19.                     e.printStackTrace(); 
  20.                 } 
  21.             } 
  22.         } 
  23.     } 
  24.     ...... 
public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LeakClass leakClass = new LeakClass();
        leakClass.start();
    }

    class LeakClass extends Thread {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(60 * 60 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    ......
}

這下就有點不太同樣了,咱們讓LeakClass繼承自Thread,而且重寫了run()方法,而後在MainActivity的onCreate()方法中去啓動LeakClass這個線程。而LeakClass的run()方法中運行了一個死循環,也就是說這個線程永遠都不會執行結束,那麼LeakClass這個對象就一直不能獲得釋放,而且它持有的MainActivity也將沒法獲得釋放,那麼內存泄露就出現了。

如今咱們能夠將程序運行起來,而後不斷地旋轉手機讓程序在橫屏和豎屏之間切換,由於每切換一次Activity都會經歷一個從新建立的過程,而前面建立的Activity又沒法獲得回收,那麼長時間操做下咱們的應用程序所佔用的內存就會愈來愈高,最終出現OutOfMemoryError。

下面我貼出一張不斷切換橫豎屏時GC日誌打印的結果圖,以下所示:

能夠看到,應用程序所佔用的內存是在不斷上升的。最可怕的是,這些內存一旦升上去了就永遠不會再降下來,直到程序崩潰爲止,由於這部分泄露的內存一直都沒法被垃圾回收器回收掉。

那麼經過上面學習的GC日誌以及DDMS工具這兩種方式,如今咱們已經能夠比較輕鬆地發現應用程序中是否存在內存泄露的現象了。可是若是真的出現了內存泄露,咱們應該怎麼定位到具體是哪裏出的問題呢?這就須要藉助一個內存分析工具了,叫作Eclipse Memory Analyzer(MAT)。咱們須要先將這個工具下載下來,下載地址是:http://eclipse.org/mat/downloads.php。這個工具分爲Eclipse插件版和獨立版兩種,若是你是使用Eclipse開發的,那麼可使用插件版MAT,很是方便。若是你是使用Android Studio開發的,那麼就只能使用獨立版的MAT了。

下載好了以後下面咱們開始學習如何去分析內存泄露的緣由,首先仍是進入到DDMS界面,而後在左側面板選中咱們要觀察的應用程序進程,接着點擊Dump HPROF file按鈕,以下圖所示:

點擊這個按鈕以後須要等待一段時間,而後會生成一個HPROF文件,這個文件記錄着咱們應用程序內部的全部數據。可是目前MAT仍是沒法打開這個文件的,咱們還須要將這個HPROF文件從Dalvik格式轉換成J2SE格式,使用hprof-conv命令就能夠完成轉換工做,以下所示:

  1. hprof-conv dump.hprof converted-dump.hprof 
hprof-conv dump.hprof converted-dump.hprof

hprof-conv命令文件存放於<Android Sdk>/platform-tools目錄下面。另外若是你是使用的插件版的MAT,也能夠直接在Eclipse中打開生成的HPROF文件,不用通過格式轉換這一步。

好的,接下來咱們就能夠來嘗試使用MAT工具去分析內存泄漏的緣由了,這裏須要提醒你們的是,MAT並不會準確地告訴咱們哪裏發生了內存泄漏,而是會提供一大堆的數據和線索,咱們須要本身去分析這些數據來去判斷究竟是不是真的發生了內存泄漏。那麼如今運行MAT工具,而後選擇打開轉換事後的converted-dump.hprof文件,以下圖所示:

MAT中提供了很是多的功能,這裏咱們只要學習幾個最經常使用的就能夠了。上圖最中央的那個餅狀圖展現了最大的幾個對象所佔內存的比例,這張圖中提供的內容並很少,咱們能夠忽略它。在這個餅狀圖的下方就有幾個很是有用的工具了,咱們來學習一下。

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

Dominator Tree會將全部內存中的對象按大小進行排序,而且咱們能夠分析對象之間的引用結構。

通常最經常使用的就是以上兩個功能了,那麼咱們先從Dominator Tree開始學起。

如今點擊Dominator Tree,結果以下圖所示:

這張圖包含的信息很是多,我來帶着你們一塊兒解析一下。首先Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存,所以從上圖中看,前兩行的Retained Heap是最大的,咱們分析內存泄漏時,內存最大的對象也是最應該去懷疑的。

另外你們應該能夠注意到,在每一行的最左邊都有一個文件型的圖標,這些圖標有的左下角帶有一個紅色的點,有的則沒有。帶有紅點的對象就表示是能夠被GC Roots訪問到的,根據上面的講解,能夠被GC Root訪問到的對象都是沒法被回收的。那麼這就說明全部帶紅色的對象都是泄漏的對象嗎?固然不是,由於有些對象系統須要一直使用,原本就不該該被回收。咱們能夠注意到,上圖當中全部帶紅點的對象最右邊都有寫一個System Class,說明這是一個由系統管理的對象,並非由咱們本身建立並致使內存泄漏的對象。

那麼上圖中就沒法看出內存泄漏的緣由了嗎?確實,內存泄漏原本就不是這麼容易找出的,咱們還須要進一步進行分析。上圖當中,除了帶有System Class的行以外,最大的就是第二行的Bitmap對象了,雖然Bitmap對象如今不能被GC Roots訪問到,但不表明着Bitmap所持有的其它引用也不會被GC Roots訪問到。如今咱們能夠對着第二行點擊右鍵 -> Path to GC Roots -> exclude weak references,爲何選擇exclude weak references呢?由於弱引用是不會阻止對象被垃圾回收器回收的,因此咱們這裏直接把它排除掉,結果以下圖所示:

能夠看到,Bitmap對象通過層層引用以後,到了MainActivity$LeakClass這個對象,而後在圖標的左下角有個紅色的圖標,就說明在這裏能夠被GC Roots訪問到了,而且這是由咱們本身建立的Thread,並非System Class了,那麼因爲MainActivity$LeakClass能被GC Roots訪問到致使不能被回收,致使它所持有的其它引用也沒法被回收了,包括MainActivity,也包括MainActivity中所包含的圖片。

經過這種方式,咱們就成功地將內存泄漏的緣由找出來了。這是Dominator Tree中比較經常使用的一種分析方式,即搜索大內存對象通向GC Roots的路徑,由於內存佔用越高的對象越值得懷疑。

接下來咱們再來學習一下Histogram的用法,回到Overview界面,點擊Histogram,結果以下圖所示:

這裏是把當前應用程序中全部的對象的名字、數量和大小所有都列出來了,須要注意的是,這裏的對象都是隻有Shallow Heap而沒有Retained Heap的,那麼Shallow Heap又是什麼意思呢?就是當前對象本身所佔內存的大小,不包含引用關係的,好比說上圖當中,byte[]對象的Shallow Heap最高,說明咱們應用程序中用了不少byte[]類型的數據,好比說圖片。能夠經過右鍵 -> List objects -> with incoming references來查看具體是誰在使用這些byte[]。

那麼經過Histogram又怎麼去分析內存泄漏的緣由呢?固然其實也能夠用和Dominator Tree中比較類似的方式,即分析大內存的對象,好比上圖中byte[]對象內存佔用很高,咱們經過分析byte[],最終也是能找到內存泄漏所在的,可是這裏我準備使用另一種更適合Histogram的方式。你們能夠看到,Histogram中是能夠顯示對象的數量的,那麼好比說咱們如今懷疑MainActivity中有可能存在內存泄漏,就能夠在第一行的正則表達式框中搜索「MainActivity」,以下所示:

能夠看到,這裏將包含「MainActivity」字樣的全部對象所有列出了出來,其中第一行就是MainActivity的實例。可是你們有沒有注意到,當前內存中是有11個MainActivity的實例的,這太不正常了,經過狀況下一個Activity應該只有一個實例纔對。其實這些對象就是因爲咱們剛纔不斷地橫豎屏切換所產生的,由於橫豎屏切換一次,Activity就會經歷一個從新建立的過程,可是因爲LeakClass的存在,以前的Activity又沒法被系統回收,那麼就出現這種一個Activity存在多個實例的狀況了。

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

若是想要查看內存泄漏的具體緣由,能夠對着任意一個MainActivity的實例右鍵 -> Path to GC Roots -> exclude weak references,結果以下圖所示:

能夠看到,咱們再次找到了內存泄漏的緣由,是由於MainActivity$LeakClass對象所致使的。

好了,這大概就是MAT工具最經常使用的一些用法了,固然這裏還要提醒你們一句,工具是死的,人是活的,MAT也沒有辦法保證必定能夠將內存泄漏的緣由找出來,仍是須要咱們對程序的代碼有足夠多的瞭解,知道有哪些對象是存活的,以及它們存活的緣由,而後再結合MAT給出的數據來進行具體的分析,這樣纔有可能把一些隱藏得很深的問題緣由給找出來。

那麼今天也是介紹了挺多內容了,本篇文章的講解就到這裏,因爲春節立刻就要到了,這也是今年的最後一篇文章,這裏先給你們拜個早年,祝你們春節快樂。放假期間但願你們能夠和我同樣,放下代碼,好好休息一段時間,所以下篇文章將會在年後更新,介紹一些高性能編碼的技巧,感興趣的朋友請繼續閱讀 Android最佳性能實踐(三)——高性能編碼優化

相關文章
相關標籤/搜索