由于Android是為移動設備開發(fā)的操作系統(tǒng)壶硅,我們在開發(fā)應用程序的時候應當始終把內存問題充分考慮在內。雖然Android系統(tǒng)擁有垃圾自動回收機制销斟,但這并不意味著我們就可以完全忽略何時去分配或釋放內存庐椒。即使我們全部按照上一篇文章中給出的編程建議來去編寫程序,還是會很有可能出現內存泄露或其它類型的內存問題蚂踊。所以约谈,唯一能夠解決問題的辦法,就是嘗試去分析應用程序的內存使用情況犁钟,那么本篇文章就會教大家如何進行分析棱诱。如果你還沒有看過前面一篇文章,建議先去閱讀Android最佳性能實踐(一)——合理管理內存涝动。
雖說現在的手機內存都已經非常大了迈勋,但是我們大家都知道,系統(tǒng)是不可能將所有的內存都分配給我們的應用程序的醋粟。沒錯靡菇,每個程序都會有可使用的內存上限,這被稱為堆大忻自浮(Heap Size)厦凤。不同的手機,堆大小也不盡相同育苟,隨著現在硬件設備不斷提高较鼓,堆大小也已經由Nexus One時的32MB,變成了Nexus 5時的192MB违柏。如果大家想要知道自己手機的堆大小是多少博烂,可以調用如下代碼:
[java]view plaincopy
ActivityManager?manager?=?(ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
intheapSize?=?manager.getMemoryClass();
結果是以MB為單位進行返回的,我們在開發(fā)應用程序時所使用的內存不能超出這個限制漱竖,否則就會出現OutOfMemoryError脖母。因此,比如說我們的程序中需要緩存一些數據闲孤,就可以根據堆大小來決定緩存數據的容量。
下面我們來討論一下Android的GC操作烤礁,GC全稱是Garbage Collection讼积,也就是所謂的垃圾回收。Android系統(tǒng)會在適當的時機觸發(fā)GC操作脚仔,一旦進行GC操作勤众,就會將一些不再使用的對象進行回收。那么哪些對象會被認為是不再使用鲤脏,并且可以被回收的呢们颜?我們來看下面一張圖:
上圖當中吕朵,每個藍色的圓圈就代表一個內存當中的對象,而圓圈之間的箭頭就是它們的引用關系窥突。這些對象有些是處于活動狀態(tài)的,而有些就已經不再被使用了。那么GC操作會從一個叫作Roots的對象開始檢查兼雄,所有它可以訪問到的對象就說明還在使用當中部默,應該進行保留,而其它的對象就表示已經不再被使用了称近,如下圖所示:
可以看到第队,目前所有黃色的對象仍然會被系統(tǒng)繼續(xù)保留,而藍色的對象就會在GC操作當中被系統(tǒng)回收掉了刨秆,這大概就是Android系統(tǒng)一次簡單的GC流程凳谦。
那么什么時候會觸發(fā)GC操作呢?這個通常都是由系統(tǒng)去決定的衡未,我們一般情況下都不需要主動通知系統(tǒng)應該去GC了(雖然我們確實可以這么做尸执,下面會講到),但是我們仍然可以去監(jiān)聽系統(tǒng)的GC過程眠屎,以此來分析我們應用程序當前的內存狀態(tài)剔交。那么怎樣才能去監(jiān)聽系統(tǒng)的GC過程呢?其實非常簡單改衩,系統(tǒng)每進行一次GC操作時岖常,都會在LogCat中打印一條日志,我們只要去分析這條日志就可以了葫督,日志的基本格式如下所示:
[plain]view plaincopy
D/dalvikvm:??,?,??
注意這里我仍然是以dalvik虛擬機來進行說明竭鞍,art情況下打印的內容也是基本類似的。橄镜。
首先第一部分GC_Reason偎快,這個是觸發(fā)這次GC操作的原因,一般情況下一共有以下幾種觸發(fā)GC操作的原因:
GC_CONCURRENT: ? 當我們應用程序的堆內存快要滿的時候洽胶,系統(tǒng)會自動觸發(fā)GC操作來釋放內存晒夹。
GC_FOR_MALLOC: ? 當我們的應用程序需要分配更多內存,可是現有內存已經不足的時候姊氓,系統(tǒng)會進行GC操作來釋放內存丐怯。
GC_HPROF_DUMP_HEAP: ? 當生成HPROF文件的時候,系統(tǒng)會進行GC操作翔横,關于HPROF文件我們下面會講到读跷。
GC_EXPLICIT: ? 這種情況就是我們剛才提到過的,主動通知系統(tǒng)去進行GC操作禾唁,比如調用System.gc()方法來通知系統(tǒng)效览∥耷校或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統(tǒng)進行GC操作的丐枉。
接下來第二部分Amount_freed哆键,表示系統(tǒng)通過這次GC操作釋放了多少內存。
然后Heap_stats中會顯示當前內存的空閑比例以及使用情況(活動對象所占內存 / 當前程序總內存)矛洞。
最后Pause_time表示這次GC操作導致應用程序暫停的時間洼哎。關于這個暫停的時間,Android在2.3的版本當中進行過一次優(yōu)化沼本,在2.3之前GC操作是不能并發(fā)進行的噩峦,也就是系統(tǒng)正在進行GC,那么應用程序就只能阻塞住等待GC結束抽兆。雖說這個阻塞的過程并不會很長识补,也就是幾百毫秒,但是用戶在使用我們的程序時還是有可能會感覺到略微的卡頓辫红。而自2.3之后凭涂,GC操作改成了并發(fā)的方式進行,就是說GC的過程中不會影響到應用程序的正常運行贴妻,但是在GC操作的開始和結束的時候會短暫阻塞一段時間切油,不過優(yōu)化到這種程度,用戶已經是完全無法察覺到了名惩。
下面是一次GC操作在LogCat中打印的日志:
可以看出澎胡,和我們上面所介紹的格式是完全一致的,最后的暫停時間31ms+7ms娩鹉,一次就是GC開始時的暫停時間攻谁,一次是結束時的暫停時間。另外可以根據進程id來區(qū)分這是哪個程序中進行的GC操作弯予,那么從上圖就可以看出這條GC日志是屬于24699這個程序的戚宦。
那么這是使用dalvik運行環(huán)境時所打印的GC日志,而自Android 4.4版本之后加入了art運行環(huán)境锈嫩,在art中打印GC日志基本和dalvik是相同的受楼,如下圖所示:
相信沒有什么難理解的地方吧,art中只是內容顯示的格式有了稍許變化呼寸,打印的主體內容仍然是不變的那槽。
好的,通過日志的方式我們可以簡單了解到系統(tǒng)的GC工作情況等舔,但是如果我們想要更加清楚地實時知曉當前應用程序的內存使用情況,只通過日志就有些力不從心了糟趾,我們需要通過DDMS中提供的工具來實現慌植。
打開DDMS界面甚牲,在左側面板中選擇你要觀察的應用程序進程,然后點擊Update Heap按鈕蝶柿,接著在右側面板中點擊Heap標簽丈钙,之后不停地點擊Cause GC按鈕來實時地觀察應用程序內存的使用情況即可,如下圖所示:
接著繼續(xù)操作我們的應用程序交汤,然后繼續(xù)點擊Cause GC按鈕雏赦,如果你發(fā)現反復操作某一功能會導致應用程序內存持續(xù)增高而不會下降的話,那么就說明這里很有可能發(fā)生內存泄漏了芙扎。
好了星岗,討論完了GC,接下來我們討論一下Android中內存泄漏的問題戒洼。大家需要知道的是俏橘,Android中的垃圾回收機制并不能防止內存泄漏的出現,導致內存泄漏最主要的原因就是某些長存對象持有了一些其它應該被回收的對象的引用圈浇,導致垃圾回收器無法去回收掉這些對象寥掐,那也就出現內存泄漏了。比如說像Activity這樣的系統(tǒng)組件磷蜀,它又會包含很多的控件甚至是圖片召耘,如果它無法被垃圾回收器回收掉的話,那就算是比較嚴重的內存泄漏情況了褐隆。
下面我們來模擬一種Activity內存泄漏的場景污它,內部類相信大家都有用過,如果我們在一個類中又定義了一個非靜態(tài)的內部類妓灌,那么這個內部類就會持有外部類的引用轨蛤,如下所示:
[java]view plaincopy
publicclassMainActivityextendsActionBarActivity?{
@Override
protectedvoidonCreate(Bundle?savedInstanceState)?{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakClass?leakClass?=newLeakClass();
}
classLeakClass?{
}
......
}
目前來看,代碼還是沒有問題的虫埂,因為雖然LeakClass這個內部類持有MainActivity的引用祥山,但是只要它的存活時間不會長于MainActivity,就不會阻止MainActivity被垃圾回收器回收掉伏。那么現在我們來將代碼進行如下修改:
[java]view plaincopy
publicclassMainActivityextendsActionBarActivity?{
@Override
protectedvoidonCreate(Bundle?savedInstanceState)?{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakClass?leakClass?=newLeakClass();
leakClass.start();
}
classLeakClassextendsThread?{
@Override
publicvoidrun()?{
while(true)?{
try{
Thread.sleep(60*60*1000);
}catch(InterruptedException?e)?{
e.printStackTrace();
}
}
}
}
......
}
這下就有點不太一樣了缝呕,我們讓LeakClass繼承自Thread,并且重寫了run()方法斧散,然后在MainActivity的onCreate()方法中去啟動LeakClass這個線程供常。而LeakClass的run()方法中運行了一個死循環(huán),也就是說這個線程永遠都不會執(zhí)行結束鸡捐,那么LeakClass這個對象就一直不能得到釋放栈暇,并且它持有的MainActivity也將無法得到釋放,那么內存泄露就出現了箍镜。
現在我們可以將程序運行起來源祈,然后不斷地旋轉手機讓程序在橫屏和豎屏之間切換煎源,因為每切換一次Activity都會經歷一個重新創(chuàng)建的過程,而前面創(chuàng)建的Activity又無法得到回收香缺,那么長時間操作下我們的應用程序所占用的內存就會越來越高手销,最終出現OutOfMemoryError。
下面我貼出一張不斷切換橫豎屏時GC日志打印的結果圖图张,如下所示:
可以看到锋拖,應用程序所占用的內存是在不斷上升的。最可怕的是祸轮,這些內存一旦升上去了就永遠不會再降下來兽埃,直到程序崩潰為止,因為這部分泄露的內存一直都無法被垃圾回收器回收掉倔撞。
那么通過上面學習的GC日志以及DDMS工具這兩種方式讲仰,現在我們已經可以比較輕松地發(fā)現應用程序中是否存在內存泄露的現象了。但是如果真的出現了內存泄露痪蝇,我們應該怎么定位到具體是哪里出的問題呢鄙陡?這就需要借助一個內存分析工具了,叫做Eclipse Memory Analyzer(MAT)躏啰。我們需要先將這個工具下載下來趁矾,下載地址是:http://eclipse.org/mat/downloads.php。這個工具分為Eclipse插件版和獨立版兩種给僵,如果你是使用Eclipse開發(fā)的毫捣,那么可以使用插件版MAT,非常方便帝际。如果你是使用Android Studio開發(fā)的蔓同,那么就只能使用獨立版的MAT了。
下載好了之后下面我們開始學習如何去分析內存泄露的原因蹲诀,首先還是進入到DDMS界面斑粱,然后在左側面板選中我們要觀察的應用程序進程,接著點擊Dump HPROF file按鈕脯爪,如下圖所示:
點擊這個按鈕之后需要等待一段時間则北,然后會生成一個HPROF文件,這個文件記錄著我們應用程序內部的所有數據痕慢。但是目前MAT還是無法打開這個文件的尚揣,我們還需要將這個HPROF文件從Dalvik格式轉換成J2SE格式,使用hprof-conv命令就可以完成轉換工作掖举,如下所示:
[plain]view plaincopy
hprof-conv?dump.hprof?converted-dump.hprof
hprof-conv命令文件存放于/platform-tools目錄下面快骗。另外如果你是使用的插件版的MAT,也可以直接在Eclipse中打開生成的HPROF文件,不用經過格式轉換這一步方篮。
好的思灌,接下來我們就可以來嘗試使用MAT工具去分析內存泄漏的原因了,這里需要提醒大家的是恭取,MAT并不會準確地告訴我們哪里發(fā)生了內存泄漏,而是會提供一大堆的數據和線索熄守,我們需要自己去分析這些數據來去判斷到底是不是真的發(fā)生了內存泄漏蜈垮。那么現在運行MAT工具,然后選擇打開轉換過后的converted-dump.hprof文件裕照,如下圖所示:
MAT中提供了非常多的功能攒发,這里我們只要學習幾個最常用的就可以了。上圖最中央的那個餅狀圖展示了最大的幾個對象所占內存的比例晋南,這張圖中提供的內容并不多惠猿,我們可以忽略它。在這個餅狀圖的下方就有幾個非常有用的工具了负间,我們來學習一下偶妖。
Histogram可以列出內存中每個對象的名字、數量以及大小政溃。
Dominator Tree會將所有內存中的對象按大小進行排序趾访,并且我們可以分析對象之間的引用結構。
一般最常用的就是以上兩個功能了董虱,那么我們先從Dominator Tree開始學起扼鞋。
現在點擊Dominator Tree,結果如下圖所示:
這張圖包含的信息非常多愤诱,我來帶著大家一起解析一下云头。首先Retained Heap表示這個對象以及它所持有的其它引用(包括直接和間接)所占的總內存,因此從上圖中看淫半,前兩行的Retained Heap是最大的溃槐,我們分析內存泄漏時,內存最大的對象也是最應該去懷疑的撮慨。
另外大家應該可以注意到竿痰,在每一行的最左邊都有一個文件型的圖標,這些圖標有的左下角帶有一個紅色的點砌溺,有的則沒有影涉。帶有紅點的對象就表示是可以被GC Roots訪問到的,根據上面的講解规伐,可以被GC Root訪問到的對象都是無法被回收的蟹倾。那么這就說明所有帶紅色的對象都是泄漏的對象嗎?當然不是,因為有些對象系統(tǒng)需要一直使用鲜棠,本來就不應該被回收肌厨。我們可以注意到,上圖當中所有帶紅點的對象最右邊都有寫一個System Class豁陆,說明這是一個由系統(tǒng)管理的對象柑爸,并不是由我們自己創(chuàng)建并導致內存泄漏的對象。
那么上圖中就無法看出內存泄漏的原因了嗎盒音?確實表鳍,內存泄漏本來就不是這么容易找出的,我們還需要進一步進行分析祥诽。上圖當中譬圣,除了帶有System Class的行之外,最大的就是第二行的Bitmap對象了雄坪,雖然Bitmap對象現在不能被GC Roots訪問到厘熟,但不代表著Bitmap所持有的其它引用也不會被GC Roots訪問到。現在我們可以對著第二行點擊右鍵 -> Path to GC Roots -> exclude weak references维哈,為什么選擇exclude weak references呢绳姨?因為弱引用是不會阻止對象被垃圾回收器回收的,所以我們這里直接把它排除掉笨农,結果如下圖所示:
可以看到就缆,Bitmap對象經過層層引用之后,到了MainActivity$LeakClass這個對象谒亦,然后在圖標的左下角有個紅色的圖標竭宰,就說明在這里可以被GC Roots訪問到了,并且這是由我們自己創(chuàng)建的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就會經歷一個重新創(chuàng)建的過程,但是由于LeakClass的存在眶痰,之前的Activity又無法被系統(tǒng)回收瘤旨,那么就出現這種一個Activity存在多個實例的情況了。
接下來對著MainActivity右鍵 ->?List objects -> with incoming references查看具體MainActivity實例凛驮,如下圖所示:
如果想要查看內存泄漏的具體原因裆站,可以對著任意一個MainActivity的實例右鍵 ->?Path to GC Roots -> exclude weak references,結果如下圖所示:
可以看到,我們再次找到了內存泄漏的原因宏胯,是因為MainActivity$LeakClass對象所導致的羽嫡。
好了,這大概就是MAT工具最常用的一些用法了肩袍,當然這里還要提醒大家一句杭棵,工具是死的,人是活的氛赐,MAT也沒有辦法保證一定可以將內存泄漏的原因找出來魂爪,還是需要我們對程序的代碼有足夠多的了解,知道有哪些對象是存活的艰管,以及它們存活的原因滓侍,然后再結合MAT給出的數據來進行具體的分析,這樣才有可能把一些隱藏得很深的問題原因給找出來牲芋。
http://blog.csdn.net/guolin_blog/article/details/42238633