Android內存泄露案例分析

一款優秀的Android應用,不只要有完善的功能,也要有良好的體驗,而性能是影響體驗的一個重要因素。內存泄露是Android開發中常見的性能問題。這篇文章,經過咱們曾經遇到的一個真實的案例,來說述一個內存泄露問題,從發現到分析定位,再到最終解決的全過程。

這裏把整個過程分爲四個階段: java

  • 第一階段,現場勘查,分析Bug現象,找出有用線索;
  • 第二階段,初步推斷,根據以前的線索,推斷可能致使Bug的緣由,而且進一步驗證推斷是否正確;
  • 第三階段,探究根源,找出致使Bug的真正緣由;
  • 第四階段,解決方案,研究如何解決問題。

現場勘查

以前咱們開發過一款應用,交給QA測試以後,發現有時候界面會卡頓,動畫不流暢。通過他反覆測試找到了規律,當連續屢次打開應用時,問題就會出現。咱們根據這個方式重現Bug時,又發現Logcat中頻繁輸出GC日誌(如圖一所示)。  windows

 

圖一 數組

這裏先簡單介紹一下GC,也就是垃圾回收機制,Android經過提供垃圾回收機制來管理內存,當內存不足時會觸發垃圾回收,回收沒用的對象,釋放內存。咱們經過兩張圖(圖2、三)來看一下垃圾回收的過程。 緩存


這裏GC Roots表示垃圾回收器對象,每一個節點表示內存中的對象,箭頭表示對象之間的引用關係,能被GC Roots直接或者間接引用到的對象ABCD,表示正在使用的對象,不能被引用到的EFG是無用對象,垃圾回收時就會被回收掉。當系統觸發一次垃圾回收時,對象EFG就會被回收。 服務器

以上就是垃圾回收的過程。在現場勘查這一階段,咱們找到兩條很是有用的線索: eclipse

  • 線索一:連續屢次打開應用以後,界面卡頓,動畫不流暢;
  • 線索二:操做過程當中,LogCat頻繁輸出GC日誌。

初步推斷

如今到第二階段,根據前一階段找到的線索,當連續屢次打開應用,界面卡頓,同時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不能回收。

代碼分析

根據以上的分析,咱們找到了引起內存泄露的代碼。

[java]  view plain copy
  1. class TestActivity{  
  2.     protected void onCreate(Bundle savedInstanceState) {  
  3.         LoadPicThread loadPicThread = new LoadPicThread();  
  4.         loadPicThread.start();  
  5.     }  
  6.     private class LoadPicThread extends Thread {      
  7.         @Override  
  8.         public void run() {  
  9.             super.run();  
  10.             while(true) {  
  11.                 …load data…  
  12.                 try {  
  13.                     Thread.sleep(1000*60*5);  
  14.                 } catch (InterruptedException e) {  
  15.                     e.printStackTrace();  
  16.                 }  
  17.             }  
  18.         }  
  19.     }  
  20. }  

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

相關文章
相關標籤/搜索