由於Android是為移動設備開發的操作系統,我們在開發應用程序的時候應當始終把內存問題充分考慮在內。雖然Android系統擁有垃圾自動回收機制,但這並不意味著我們就可以完全忽略何時去分配或釋放內存。即使我們全部按照上一篇文章中給出的編程建議來去編寫程序,還是會很有可能出現內存洩露或其它類型的內存問題。所以,唯一能夠解決問題的辦法,就是嘗試去分析應用程序的內存使用情況,那麼本篇文章就會教大家如何進行分析。如果你還沒有看過前面一篇文章,建議先去閱讀 Android最佳性能實踐(一)——合理管理內存 。雖說現在的手機內存都已經非常大了,但是我們大家都知道,系統是不可能將所有的內存都分配給我們的應用程序的。沒錯,每個程序都會有可使用的內存上限,這被稱為堆大小(Heap Size)。不同的手機,堆大小也不盡相同,隨著現在硬件設備不斷提高,堆大小也已經由Nexus One時的32MB,變成了Nexus 5時的192MB。如果大家想要知道自己手機的堆大小是多少,可以調用如下代碼:
[java] view plain copyActivityManager 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流程。那麼什麼時候會觸發GC操作呢?這個通常都是由系統去決定的,我們一般情況下都不需要主動通知系統應該去GC了(雖然我們確實可以這麼做,下面會講到),但是我們仍然可以去監聽系統的GC過程,以此來分析我們應用程序當前的內存狀態。那麼怎樣才能去監聽系統的GC過程呢?其實非常簡單,系統每進行一次GC操作時,都會在LogCat中打印一條日志,我們只要去分析這條日志就可以了,日志的基本格式如下所示:
[plain] view plain copy
在CODE上查看代碼片派生到我的代碼片D/dalvikvm: , , 注意這裡我仍然是以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內存洩漏的場景,內部類相信大家都有用過,如果我們在一個類中又定義了一個非靜態的內部類,那麼這個內部類就會持有外部類的引用,如下所示:
[java] view plain copy
在CODE上查看代碼片派生到我的代碼片
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被垃圾回收器回收。那麼現在我們來將代碼進行如下修改:
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片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命令就可以完成轉換工作,如下所示:[plain] view plain copy 在CODE上查看代碼片派生到我的代碼片hprof-conv dump.hprof converted-dump.hprof hprof-conv命令文件存放於/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最佳性能實踐(三)——高性能編碼優化 。