這裏把整個過程分爲四個階段: java
以前咱們開發過一款應用,交給QA測試以後,發現有時候界面會卡頓,動畫不流暢。通過他反覆測試找到了規律,當連續屢次打開應用時,問題就會出現。咱們根據這個方式重現Bug時,又發現Logcat中頻繁輸出GC日誌(如圖一所示)。 windows
圖一 數組
這裏先簡單介紹一下GC,也就是垃圾回收機制,Android經過提供垃圾回收機制來管理內存,當內存不足時會觸發垃圾回收,回收沒用的對象,釋放內存。咱們經過兩張圖(圖2、三)來看一下垃圾回收的過程。 緩存
這裏GC Roots表示垃圾回收器對象,每一個節點表示內存中的對象,箭頭表示對象之間的引用關係,能被GC Roots直接或者間接引用到的對象ABCD,表示正在使用的對象,不能被引用到的EFG是無用對象,垃圾回收時就會被回收掉。當系統觸發一次垃圾回收時,對象EFG就會被回收。 服務器
以上就是垃圾回收的過程。在現場勘查這一階段,咱們找到兩條很是有用的線索: eclipse
如今到第二階段,根據前一階段找到的線索,當連續屢次打開應用,界面卡頓,同時Logcat不斷輸出GC日誌,初步推測咱們的應用中存在內存泄漏。首先咱們先看一下什麼是內存泄露呢?咱們經過兩張張圖來演示,如圖四和五。 ide
這張圖跟剛纔演示GC過程的圖很像,這時候再觸發GC時,EG會被回收,F對於應用來講雖然無用了,卻沒法被回收,最後致使了內存泄漏。 工具
所以,極可能每次打開應用時,都會產生像F這樣的對象致使,內存佔用愈來愈高,系統頻繁觸發GC。 性能
接下來就要去驗證,這裏咱們利用DDMS工具。 測試
DDMS是虛擬機調試監控服務,它能幫助咱們測試設備截屏,設置虛擬地理座標,針對特定的進程查看它的堆信息等等。
如何利用它來驗證咱們的推斷呢?首先要load出應用的內存快照,這裏分爲4步,第一步,選中咱們要查看的應用,第二步點擊Update Heap按鈕,這時候DDMS就會通知應用準備收集內存信息,第三步選擇Heap標籤,heap標籤頁可以展現出內存的全部信息。第四步點擊Cause GC,這時候就會把內存快照load出來。這樣DDMS就把內存快照load出來了。具體操做如圖六。
圖六
Load出內存信息以後,就來分析咱們應用中是否存在內存泄漏,分析內存泄漏的關鍵的數據之一,就是Total Size。
重複打開應用時,若是不存在內存泄漏的話,Total Size只會在必定範圍內波動。若是咱們的推斷正確,連續打開應用,Total Size會持續增長。接着咱們就來測試分析,連續打開應用,如圖七。
圖七
這裏展現了第一二三,以及第十次打開時Total size的截圖,Total Size一直在增大,其中1-byte array增大最爲明顯,1-byte array表示的是byte[],或者boolean[]類型的數組。因此咱們可以得出結論:打開應用時,確實存在內存泄漏。
確認了問題,接下來就要探究問題的根源。每個應用運行過程當中,都會持有上萬甚至百萬個對象,咱們就要分析這些對象在內存中的狀態,看哪些對象對應用來講已經沒用了,可是還在佔用着內存。
這個過程咱們用到了MAT。MAT是一款功能豐富,運行速度很是快的堆內存分析工具。它可以快速的分析堆中的全部對象,計算出每一個對象佔有的內存大小。它的功能很是強大,分析完內存以後,它還可以幫找出可能致使內存泄漏的對象,列出佔用內存比較大的對象,它提供查詢java容器對象使用率的等功能,這些功能對於咱們分析應用的內存都很是有幫助。
它既有獨立的安裝程序,也有針對eclipse的插件,咱們根據本身的需求下載相應的程序。咱們使用的時候也很是簡單,能夠利用剛纔介紹的DDMS工具,把內存快照導出到.hprof文件中,而後MAT直接打開這個hprof文件就好了。獨立安裝程序的下載地址: http://www.eclipse.org/mat/。
根據前面的測試,咱們通過幾回操做就致使1-byte array的Total Size從20M增大到70M,平均每次增長5M左右,這個size是比較大的,所以推斷有內存佔用比較大的對象致使的內存泄露。結合MAT的Dominator Tree功能,咱們來着手分析,Dominator Tree能列出內存中全部對象,以及他們佔用內存的大小。
圖八
這裏是Dominator Tree的一張截圖,先介紹兩個名詞第一個Shallow Heap,表示對象自己的內存大小,包括對象的頭以及成員變量等,第二個Retained Heap表示:一個對象自己以及它持有的全部對象的內存總和,也就是GC時,回收一個對象所釋放的全部內存空間。從這張圖中能夠看到,Retained Heap最大的時Resources對象,可是Resource是System Class對象,也就是系統管理的對象,也不會是引發咱們內存泄漏的緣由,咱們不用去分析它。
第二大的就是Bitmap對象。從前面的介紹咱們已經知道,若是一個對象能被GC Roots直接或者間接引用,它就不能被回收,那咱們就來看一下Bitmap到GC Roots的引用路徑,看Bitmap時被哪一個對象持有的。選中Bitmap,右鍵選擇,Path To GC Roots,再選擇execlude weak references,由於弱引用是不能阻止垃圾回收的,因此咱們直接排除弱引用。
下面圖九就是Bitmap到GC Roots的引用路徑。其中LoadPicThread對象前面有個小紅點,這個小紅點就表示這個對象是被GC Roots直接持有的。
圖九
因此整個引用路徑就是GC Roots引用着Thread,Thread引用着咱們的Activity,而Activity中包含了Bitmap對象。
這時候當前界面已經退出了,可是Thread 仍持有着Activity 的引用,致使Activity 和它引用的內存例如Bitmap不能被回收。這時候問題的真相基本浮出水面了。
爲了進一步確認咱們的結果,咱們從另外一個角度進行驗證,看內存中是否多個被Thread持有的,不能回收的Activity的對象?藉助MAT的Histogram功能,它能列出內存中的全部類,以及每一個類的實例個數。
圖十
如圖十,MAT提供了正則搜索的功能,能夠根據類名搜索,咱們這裏搜索獲得的結果是11個Activity對象,因此進一步驗證成功。就是由於咱們建立的那個Thread持有着Acitivy的對象,致使關閉以後Activiy不能回收。
根據以上的分析,咱們找到了引起內存泄露的代碼。
LoadPicThread是TestActivity的一個內部類,它隱式的持有着TestActivity的實例,LoadPicThread會每5分鐘去服務器請求一次數據,這個Thread一直都不會結束,並且每次打開界面時都會建立一個這樣的Thread。因此咱們這裏致使內存泄漏的根本緣由就是長生命週期對象(Thread)持有短生命週期對象(Activity)的引用,致使Activiy退出以後,不能被回收。
最後到解決問題階段,找到問題以後怎麼解決呢?咱們想到了兩種解決方案。
第一,將Thread從Activity移除,能夠放到後臺服務中,這樣Activity與Thread之間就不會相互依賴,若是Thread要作的事情跟Activity業務邏輯不是很緊密,例如在一些數據緩存的操做,這時候就能夠用這種方案。
第二,當Activity結束時,中止Thead,讓Thread與Activity的生命週期保持一致,通常能夠在onDestory方法中,給Thread發送一個結束信號。
以上是就是咱們從發現到解決內存泄漏的整個過程。其實在Android開發過程當中,不少錯誤的代碼,引起內存泄露。例如,不當的使用Context;構造Adapter時,沒有使用緩存的convertView等等。
最後總結一下:
第一,做爲Android開發人員,只有深入理解Android經常使用組件的工做機制,以及應用中各個對象的生命週期,才能儘可能避免寫出致使內存泄露的代碼;
第二,當程序出現問題時,首先要找到觸發它的場景,就像這個案例中,咱們根據QA提供的重現方式,通過反覆測試和觀察,最終定位到問題。而在咱們平常開發中,可能遇到更加複雜的問題,在面對複雜的狀況下,只有找到觸發問題的關鍵場景,咱們才能快速的定位問題,並加以解決。
第三,強大的工具是幫助咱們分析和定位問題的利器,例如前面用到的DDMS和MAT工具,他們可以讓咱們可以深刻到應用的內部進行探索和研究,從而快速的分析到問題的根源。因此開發人員應該學會運用這些強大的工具,來分析解決各類疑難問題。
DDMS抓取的hprof文件不能直接在MAT中打開, 因此須要轉換一下:
hprof-conv在D:\Soft\TVMaosoft\adt-bundle-windows-x86_64-20140702\sdk\platform-tools 下
命令
C:\Users\gaoshuai>hprof-conv C:\Users\gaoshuai\Desktop\com.kookong.tv.hprof a.
hprof