Android內存泄漏分析及調試

尊重原創作者,轉載請註明出處:

http://blog.csdn.net/gemmem/article/details/13017999

此文承接我的另一篇文章:Android進程的內存管理分析

首先了解一下dalvik的Garbage Collection:

如上圖所示,GC會選擇一些它了解還存活的對象作為內存遍歷的根節點(GC Roots),比方說thread stack中的變數,JNI中的全局變數,zygote中的對象(class loader載入)等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。如下圖藍色部分。

Java內存泄漏指的是進程中某些對象(垃圾對象)已經沒有使用價值了,但是它們卻可以直接或間接地引用到gc roots導致無法被GC回收。無用的對象佔據著內存空間,使得實際可使用內存變小,形象地說法就是內存泄漏了。下面分析一些可能導致內存泄漏的情景。

常見的內存泄漏

1、非靜態內部類的靜態實例容易造成內存泄漏

[java]view plaincopy

  1. publicclassMainActivityextendsActivity
  2. {
  3. staticDemosInstance=null;
  4. @Override
  5. publicvoidonCreate(BundlesavedInstanceState)
  6. {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. if(sInstance==null)
  10. {
  11. sInstance=newDemo();
  12. }
  13. }
  14. classDemo
  15. {
  16. voiddoSomething()
  17. {
  18. System.out.print("dosth.");
  19. }
  20. }
  21. }

[java]view plaincopy

  1. publicclassMainActivityextendsActivity
  2. {
  3. staticDemosInstance=null;
  4. @Override
  5. publicvoidonCreate(BundlesavedInstanceState)
  6. {
  7. super.onCreate(savedInstanceState);
  8. setContentView(R.layout.activity_main);
  9. if(sInstance==null)
  10. {
  11. sInstance=newDemo();
  12. }
  13. }
  14. classDemo
  15. {
  16. voiddoSomething()
  17. {
  18. System.out.print("dosth.");
  19. }
  20. }
  21. }

上面的代碼中的sInstance實例類型為靜態實例,在第一個MainActivity act1實例創建時,sInstance會獲得並一直持有act1的引用。當MainAcitivity銷毀後重建,因為sInstance持有act1的引用,所以act1是無法被GC回收的,進程中會存在2個MainActivity實例(act1和重建後的MainActivity實例),這個act1對象就是一個無用的但一直佔用內存的對象,即無法回收的垃圾對象。所以,對於lauchMode不是singleInstance的Activity, 應該避免在activity裡面實例化其非靜態內部類的靜態實例。

2、activity使用靜態成員

[java]view plaincopy

  1. privatestaticDrawablesBackground;
  2. @Override
  3. protectedvoidonCreate(Bundlestate){
  4. super.onCreate(state);
  5. TextViewlabel=newTextView(this);
  6. label.setText("Leaksarebad");
  7. if(sBackground==null){
  8. sBackground=getDrawable(R.drawable.large_bitmap);
  9. }
  10. label.setBackgroundDrawable(sBackground);
  11. setContentView(label);
  12. }

[java]view plaincopy

  1. privatestaticDrawablesBackground;
  2. @Override
  3. protectedvoidonCreate(Bundlestate){
  4. super.onCreate(state);
  5. TextViewlabel=newTextView(this);
  6. label.setText("Leaksarebad");
  7. if(sBackground==null){
  8. sBackground=getDrawable(R.drawable.large_bitmap);
  9. }
  10. label.setBackgroundDrawable(sBackground);
  11. setContentView(label);
  12. }

由於用靜態成員sBackground緩存了drawable對象,所以activity載入速度會加快,但是這樣做是錯誤的。因為在android 2.3系統上,它會導致activity銷毀後無法被系統回收。

label .setBackgroundDrawable函數調用會將label賦值給sBackground的成員變數mCallback。

上面代碼意味著:sBackground(GC Root)會持有TextView對象,而TextView持有Activity對象。所以導致Activity對象無法被系統回收。

下面看看android4.0為了避免上述問題所做的改進。

先看看android 2.3的Drawable.Java對setCallback的實現:

public final void setCallback(Callback cb){

mCallback = cb;

}

再看看android 4.0的Drawable.Java對setCallback的實現:

public final void setCallback(Callback cb){

mCallback = newWeakReference<Callback> (cb);

}

在android 2.3中要避免內存泄漏也是可以做到的, 在activity的onDestroy時調用

sBackgroundDrawable.setCallback(null)。

以上2個例子的內存泄漏都是因為Activity的引用的生命周期超越了activity對象的生命周期。也就是常說的Context泄漏,因為activity就是context。

想要避免context相關的內存泄漏,需要注意以下幾點:

·不要對activity的context長期引用(一個activity的引用的生存周期應該和activity的生命周期相同)

·如果可以的話,盡量使用關於application的context來替代和activity相關的context

·如果一個acitivity的非靜態內部類的生命周期不受控制,那麼避免使用它;正確的方法是使用一個靜態的內部類,並且對它的外部類有一WeakReference,就像在ViewRootImpl中內部類W所做的那樣。

3、使用handler時的內存問題

我們知道,Handler通過發送Message與主線程交互,Message發出之後是存儲在MessageQueue中的,有些Message也不是馬上就被處理的。在Message中存在一個 target,是Handler的一個引用,如果Message在Queue中存在的時間越長,就會導致Handler無法被回收。如果Handler是非靜態的,則會導致Activity或者Service不會被回收。 所以正確處理Handler等之類的內部類,應該將自己的Handler定義為靜態內部類。

HandlerThread的使用也需要注意:

當我們在activity裡面創建了一個HandlerThread,代碼如下:

[java]view plaincopy

  1. publicclassMainActivityextendsActivity
  2. {
  3. @Override
  4. publicvoidonCreate(BundlesavedInstanceState)
  5. {
  6. super.onCreate(savedInstanceState);
  7. setContentView(R.layout.activity_main);
  8. ThreadmThread=newHandlerThread("demo",Process.THREAD_PRIORITY_BACKGROUND);
  9. mThread.start();
  10. MyHandlermHandler=newMyHandler(mThread.getLooper());
  11. …….
  12. …….
  13. …….
  14. }
  15. @Override
  16. publicvoidonDestroy()
  17. {
  18. super.onDestroy();
  19. }
  20. }

[java]view plaincopy

  1. publicclassMainActivityextendsActivity
  2. {
  3. @Override
  4. publicvoidonCreate(BundlesavedInstanceState)
  5. {
  6. super.onCreate(savedInstanceState);
  7. setContentView(R.layout.activity_main);
  8. ThreadmThread=newHandlerThread("demo",Process.THREAD_PRIORITY_BACKGROUND);
  9. mThread.start();
  10. MyHandlermHandler=newMyHandler(mThread.getLooper());
  11. …….
  12. …….
  13. …….
  14. }
  15. @Override
  16. publicvoidonDestroy()
  17. {
  18. super.onDestroy();
  19. }
  20. }

這個代碼存在泄漏問題,因為HandlerThread的run方法是一個死循環,它不會自己結束,線程的生命周期超過了activity生命周期,當橫豎屏切換,HandlerThread線程的數量會隨著activity重建次數的增加而增加。

應該在onDestroy時將線程停止掉:mThread.getLooper().quit();

另外,對於不是HandlerThread的線程,也應該確保activity消耗後,線程已經終止,可以這樣做:在onDestroy時調用mThread.join();

4、註冊某個對象後未反註冊

註冊廣播接收器、註冊觀察者等等,比如:

假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息(如信號強度等),則可以在LockScreen中定義一個PhoneStateListener的對象,同時將它註冊到TelephonyManager服務中。對於LockScreen對象,當需要顯示鎖屏界面的時候就會創建一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。

  但是如果在釋放LockScreen對象的時候忘記取消我們之前註冊的PhoneStateListener對象,則會導致LockScreen無法被GC回收。如果不斷的使鎖屏界面顯示和消失,則最終會由於大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process進程掛掉。

雖然有些系統程序,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在我們的程序中明確的取消註冊,程序結束時應該把所有的註冊都取消掉。

5、集合中對象沒清理造成的內存泄露

  我們通常把一些對象的引用加入到了集合中,當我們不需要該對象時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。

比如某公司的ROM的鎖屏曾經就存在內存泄漏問題:

這個泄漏是因為LockScreen每次顯示時會註冊幾個callback,它們保存在KeyguardUpdateMonitor的ArrayList<InfoCallback>、ArrayList<SimStateCallback>等ArrayList實例中。但是在LockScreen解鎖後,這些callback沒有被remove掉,導致ArrayList不斷增大, callback對象不斷增多。這些callback對象的size並不大,heap增長比較緩慢,需要長時間地使用手機才能出現OOM,由於鎖屏是駐留在system_server進程里,所以導致結果是手機重啟。

6、資源對象沒關閉造成的內存泄露

  資源性對象比如(Cursor,File文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。它們的緩衝不僅存在於Java虛擬機內,還存在於Java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄露。因為有些資源性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性對象在不使用的時候,應該立即調用它的close()函數,將其關閉掉,然後再置為null.在我們的程序退出時一定要確保我們的資源性對象已經關閉。

  程序中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在長時間大量操作的情況下才會復現內存問題,這樣就會給以後的測試和問題排查帶來困難和風險。

7、一些不良代碼成內存壓力

有些代碼並不造成內存泄露,但是它們或是對沒使用的內存沒進行有效及時的釋放,或是沒有有效的利用已有的對象而是頻繁的申請新內存,對內存的回收和分配造成很大影響的,容易迫使虛擬機不得不給該應用進程分配更多的內存,增加vm的負擔,造成不必要的內存開支。

7.1,Bitmap使用不當

第一、及時的銷毀。

雖然,系統能夠確認Bitmap分配的內存最終會被銷毀,但是由於它佔用的內存過多,所以很可能會超過Java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機一個暗示:「該圖片可以釋放了」。

第二、設置一定的採樣率。

有時候,我們要顯示的區域很小,沒有必要將整個圖片都載入出來,而只需要記載一個縮小過的圖片,這時候可以設置一定的採樣率,那麼就可以大大減小佔用的內存。如下面的代碼:

[java]view plaincopy

  1. privateImageViewpreview;
  2. BitmapFactory.Optionsoptions=newBitmapFactory.Options();
  3. options.inSampleSize=2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一
  4. Bitmapbitmap=BitmapFactory.decodeStream(cr.openInputStream(uri),null,options);preview.setImageBitmap(bitmap);

[java]view plaincopy

  1. privateImageViewpreview;
  2. BitmapFactory.Optionsoptions=newBitmapFactory.Options();
  3. options.inSampleSize=2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一
  4. Bitmapbitmap=BitmapFactory.decodeStream(cr.openInputStream(uri),null,options);preview.setImageBitmap(bitmap);

第三、巧妙的運用軟引用(SoftRefrence)

有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法調用Recycle函數。這時候巧妙的運用軟引用,可以使Bitmap在內存快不足時得到有效的釋放。如下:

[java]view plaincopy

  1. SoftReference<Bitmap>bitmap_ref=newSoftReference<Bitmap>(BitmapFactory.decodeStream(inputstream));
  2. ……
  3. ……
  4. if(bitmap_ref.get()!=null)
  5. bitmap_ref.get().recycle();

[java]view plaincopy

  1. SoftReference<Bitmap>bitmap_ref=newSoftReference<Bitmap>(BitmapFactory.decodeStream(inputstream));
  2. ……
  3. ……
  4. if(bitmap_ref.get()!=null)
  5. bitmap_ref.get().recycle();

7.2,構造Adapter時,沒有使用緩存的 convertView

  以構造ListView的BaseAdapter為例,在BaseAdapter中提共了方法:

  public View getView(intposition, View convertView, ViewGroup parent)

  來向ListView提供每一個item所需要的view對象。初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的view對象,同時ListView會將這些view對象緩存起來。當向上滾動ListView時,原先位於最上面的list item的view對象會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被緩存起來的list item的view對象(初始化時緩存中沒有view對象則convertView是null)。

  由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新實例化一個View對象的話,即浪費時間,也造成內存垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機將不得不給該應用進程分配更多的內存,造成不必要的內存開支。ListView回收list item的view對象的過程可以查看:

  android.widget.AbsListView.Java--> void addScrapView(View scrap) 方法。

  Java代碼:

[java]view plaincopy

  1. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  2.   Viewview=newXxx(...);
  3.   returnview;
  4.   }

[java]view plaincopy

  1. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  2.   Viewview=newXxx(...);
  3.   returnview;
  4.   }

  修正示例代碼:

  Java代碼:

[java]view plaincopy

  1.   publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  2.   Viewview=null;
  3.   if(convertView!=null){
  4.   view=convertView;
  5.   populate(view,getItem(position));
  6.   }else{
  7.   view=newXxx(...);
  8.   }
  9.   returnview;
  10.   }

[java]view plaincopy

  1. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  2. Viewview=null;
  3. if(convertView!=null){
  4. view=convertView;
  5. populate(view,getItem(position));
  6. }else{
  7. view=newXxx(...);
  8. }
  9. returnview;
  10. }

7.3、不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用 hashtable , vector 創建一組對象容器,然後從容器中去取那些對象,而不用每次 new 之後又丟棄。

關於內存泄漏的調試

(1).內存監測工具 DDMS --> Heap無論怎麼小心,想完全避免bad code是不可能的,此時就需要一些工具來幫助我們檢查代碼中是否存在會造成內存泄漏的地方。Android tools中的DDMS就帶有一個很不錯的內存監測工具Heap(這裡我使用eclipse的ADT插件,並以真機為例,在模擬器中的情況類似)。用 Heap監測應用進程使用內存情況的步驟如下:1. 啟動eclipse後,切換到DDMS透視圖,並確認Devices視圖、Heap視圖都是打開的;2. 將手機通過USB鏈接至電腦,鏈接時需要確認手機是處於「USB調試」模式,而不是作為「MassStorage」;3. 鏈接成功後,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;4. 點擊選中想要監測的進程,比如system_process進程;5. 點擊選中Devices視圖界面中最上方一排圖標中的「Update Heap」圖標;6. 點擊Heap視圖中的「Cause GC」按鈕;7. 此時在Heap視圖中就會看到當前選中的進程的內存使用量的詳細情況。說明:a) 點擊「Cause GC」按鈕相當於向虛擬機請求了一次gc操作;b) 當內存使用信息第一次顯示以後,無須再不斷的點擊「CauseGC」,Heap視圖界面會定時刷新,在對應用的不斷的操作過程中就可以看到內存使用的變化;c) 內存使用信息的各項參數根據名稱即可知道其意思,在此不再贅述。 如何才能知道我們的程序是否有內存泄漏的可能性呢。這裡需要注意一個值:Heap視圖中部有一個Type叫做dataobject,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是「Total Size」,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:a) 不斷的操作當前應用,同時注意觀察data object的Total Size值;b) 正常情況下Total Size值都會穩定在一個有限的範圍內,也就是說由於程序中的的代碼良好,沒有造成對象不被垃圾回收的情況,所以說雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;c) 反之如果代碼中存在沒有釋放對象引用的情況,則dataobject的Total Size值在每次GC後不會有明顯的回落,隨著操作次數的增多Total Size的值會越來越大, 直到到達一個上限後導致進程OOM被kill掉。

(2).內存分析工具 MAT(Memory Analyzer Tool)

並不是所有的內存泄漏都可以用觀察heap size的方法檢測出來,因為有的程序只是泄漏了幾個對象,而且泄漏的對象個數不會隨著程序的運行而增加,這種內存泄漏不會直接導致OOM,但是無用對象無法回收,無疑是對內存的浪費,會影響到程序的性能,我們需要使用MAT工具才能發現這種比較隱蔽的內存泄漏。使用MAT之前有2個概念是要掌握的:Shallowheap和Retained heap。Shallow heap表示對象本身所佔內存大小,一個內存大小100bytes的對象Shallow heap就是100bytes。Retained heap表示通過回收這一個對象總共能回收的內存,比方說一個100bytes的對象還直接或者間接地持有了另外3個100bytes的對象引用,回收這個對象的時候如果另外3個對象沒有其他引用也能被回收掉的時候,Retained heap就是400bytes。

MAT使用Dominator Tree這樣一種來自圖形理論的概念。

所謂Dominator,就是Flow Graph中從源節點出發到某個節點的的必經節點。那麼根據這個概念我們可以從上圖左側的Flow Graph構造出右側的Dominator Tree。這樣一來很容易就看出每個節點的Retained heap了。Shallow heap和Retained heap在MAT中是非常有用的概念,用於內存泄漏的分析。

我們做一個Demo。在工程的MainActivity當中加入如下代碼:

[java]view plaincopy

  1. publicclassMainActivityextendsActivity{
  2. staticLeakyleak=null;
  3. classLeaky{
  4. voiddoSomething(){
  5. System.out.println("Wheee!!!");
  6. }
  7. }
  8. @Override
  9. publicvoidonCreate(BundlesavedInstanceState){
  10. super.onCreate(savedInstanceState);
  11. if(leak==null){
  12. leak=newLeaky();
  13. }
  14. ...

[java]view plaincopy

  1. publicclassMainActivityextendsActivity{
  2. staticLeakyleak=null;
  3. classLeaky{
  4. voiddoSomething(){
  5. System.out.println("Wheee!!!");
  6. }
  7. }
  8. @Override
  9. publicvoidonCreate(BundlesavedInstanceState){
  10. super.onCreate(savedInstanceState);
  11. if(leak==null){
  12. leak=newLeaky();
  13. }
  14. ...

上面這段代碼,對Java熟悉的同學都應該了解非靜態內部類對象默認持有外部類對象引用,而leak作為靜態變數在非空判斷下只產生了一個對象,因此當旋轉屏幕時生成新的Activity的時候舊的Activity的引用依然被持有,如下圖:

通過觀察旋轉屏幕前後Log中GC的信息也能看出heap的data object分配往上漲了一點,並且在GC執行完heap的分配穩定之後並沒有降下來,這就是內存泄漏的跡象。

我們通過MAT來進行分析。先下載MAT,可以作為Eclipse插件下載,也可以作為RCP應用下載,本質上沒有區別。DDMS中選中應用對應的進程名,點擊Dump HPROF file的按鈕,等一小段時間生成HPROF文件,如果是Eclipse插件的話,Eclipse會為這個HPROF自動轉化成標準的HPROF並自動打開MAT分析界面。如果是作為RCP應用的話,需要用sdk目錄tools中的hprof-conv工具來進行轉化,也就是上文提及的命令hprof-convorig.hprof converted.hprof,這種方式保存HPROF文件的位置選擇更為自主,你也可以修改Eclipse的設置讓Eclipse提示保存而不是自動打開,在Preferences -> Android -> DDMS中的HPROFAction由Open in Eclipse改為Save todisk。打開MAT,選擇轉化好的HPROF文件,可以看到Overview的界面如下圖:

中間的餅狀圖就是根據我們上文所說的Retained heap的概念得到的內存中一些Retained Size最大的對象。點擊餅狀圖能看到這些對象類型,但對內存泄漏的分析還遠遠不夠。再看下方Action中有Dominator Tree和Histogram的選項,這一般來說是最有用的工具。還記得我們上文說過的DominatorTree的概念嗎,這就是我們用來跟蹤內存泄漏的方式。點開Dominator Tree,會看到以Retained heap排序的一系列對象,如下圖:

Resources類型對象由於一般是系統用於載入資源的,所以Retained heap較大是個比較正常的情況。但我們注意到下面的Bitmap類型對象的Retained heap也很大,很有可能是由於內存泄漏造成的。所以我們右鍵點擊這行,選擇Path To GC Roots ->exclude weak references,可以看到下圖的情形:

Bitmap最終被leak引用到,這應該是一種不正常的現象,內存泄漏很可能就在這裡了。MAT不會告訴哪裡是內存泄漏,需要你自行分析,由於這是Demo,是我們特意造成的內存泄漏,因此比較容易就能看出來,真實的應用場景可能需要你仔細的進行分析。

根據我們上文介紹的Dominator的概念,leak對象是該Bitmap對象的Dominator,應該出現在Dominator Tree視圖裡面,但實際上卻沒有。這是由於MAT並沒有對weak references做區別對待,這也是我們選擇exclude weakreferences的原因。如果我們Path To GC Roots ->with all references,我們可以看到下圖的情形:

可以看到還有另外一個對象在引用著這個Bitmap對象,了解weak references的同學應該知道GC是如何處理weak references,因此在內存泄漏分析的時候我們可以把weak references排除掉。

有些同學可能希望根據某種類型的對象個數來分析內存泄漏。我們在Overview視圖中選擇Actions -> Histogram,可以看到類似下圖的情形:

上圖展示了內存中各種類型的對象個數和Shallow heap,我們看到byte[]佔用Shallow heap最多,那是因為Honeycomb之後Bitmap Pixel Data的內存分配在Dalvik heap中。右鍵選中byte[]數組,選擇List Objects -> with incomingreferences,可以看到byte[]具體的對象列表:

我們發現第二個byte[]的Retained heap較大,內存泄漏的可能性較大,因此右鍵選中這行,Path To GC Roots -> exclude weak references,同樣可以看到上文所提到的情況,我們的Bitmap對象被leak所引用到,這裡存在著內存泄漏。

在Histogram視圖中第一行<Regex>中輸入com.example.android.hcgallery,過濾出我們自己應用中的類型,如下圖:

我們發現本應該只有一個MainActivity現在卻有兩個,顯然不正常。右鍵選擇List Objects-> with incoming references,可以看到這兩個具體的MainActivity對象。右鍵選中Retained heap較大的MainActivity,Path To GC Roots -> exclude weak references,再一次可疑對象又指向了leak對象。

以上是MAT一些基本的用法,如果你感興趣,可以自行深入的去了解MAT的其他功能


推薦閱讀:

GoogleDeveloperDay 回顧
谷歌為什麼一直不改進Android「下載」這個應用?
Android我還可以相信你多少系列文章四之懸浮窗
如何評價 國內 android 手機 和 國內 android ROM?

TAG:Android | Android內存 | 分析 |