內存泄露原因及解決方案

一、 Android的內存機制
Android的程序由Java語言編寫,所以Android的內存管理與Java的內存管理相似。程序員通過new爲對象分配內存,所有對象在java堆內分配空間;然而對象的釋放是由垃圾回收器來完成的.
那麼GC怎麼能夠確認某一個對象是不是已經被廢棄了呢?Java採用了有向圖的原理。Java將引用關係考慮爲圖的有向邊,有向邊從引用者指向引用對象。線程對象可以作爲有向圖的起始頂點,該圖就是從起始頂點開始的一棵樹,根頂點可以到達的對象都是有效對象,GC不會回收這些對象。如果某個對象(連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼我們認爲這個(這些)對象不再被引用,可以被GC回收。


二、Android的內存溢出

1、內存泄露導致

由於我們程序的失誤,長期保持某些資源(如Context)的引用,造成內存泄露,資源造成得不到釋放。 

Android 中常見就是Activity 被引用沒有在調用finish之後卻沒有釋放,第二次打開activity又重新創建,這樣的內存泄露則會導致內存的溢出。
2、佔用內存較多的對象

 保存了多個耗用內存過大的對象(如Bitmap)或加載單個超大的圖片,造成內存超出限制。

三、常見的內存泄漏
1.萬惡的static
 static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。

    privatestatic ActivitymContext;      //省略 

 如何纔能有效的避免這種引用的發生呢?

   第一,應該儘量避免static成員變量引用資源耗費過多的實例,比如Context。

   第二、Context儘量使用ApplicationContext,因爲Application的Context的生命週期比較長,引用它不會出現內存泄露的問題。

   第三、使用WeakReference代替強引用。比如可以使用WeakReference mContextRef;

2.線程惹的禍
線程也是造成內存泄露的一個重要的源頭。線程產生內存泄露的主要原因在於線程生命週期的不可控。我們來考慮下面一段代碼。

 public class MyActivity extends Activity {       
@Override       
public void onCreate(Bundle savedInstanceState) {           
  super.onCreate(savedInstanceState);           
  setContentView(R.layout.main);           
  new MyThread().start();       
}         
private class MyThread extends Thread{           
@Override           
  public void run() {               
  super.run();               
  //do somthing           
}       
}   
}   
 我們思考一個問題:假設MyThread的run函數是一個很費時的操作,當調用finish的時候Activity會銷燬掉嗎?

 

  事實上由於我們的線程是Activity的內部類,所以MyThread中保存了Activity的一個引用,當MyThread的run函數沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了內存泄露的問題。

 

解決方案


   第一、將線程的內部類,改爲靜態內部類。

   第二、如果需要引用Acitivity,使用弱引用。
    
    另外在使用handler的時候, 尤其用到循環調用的時候,在Activity 退出的時候注意移除。否則也會導致泄露

 public class ThreadDemo extends Activity {    
    private static final String TAG = "ThreadDemo";    
    private int count = 0;    
    private Handler mHandler =  new Handler();    
        
    private Runnable mRunnable = new Runnable() {    
            
        public void run() {    
            //爲了方便 查看,我們用Log打印出來     
            Log.e(TAG, Thread.currentThread().getName() + " " +count);      
            //每2秒執行一次     
            mHandler.postDelayed(mRunnable, 2000);    
        }     
    };    
    @Override    
    public void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);    
        setContentView(R.layout.main);     
        //通過Handler啓動線程     
        mHandler.post(mRunnable);    
    }    
        
}  
//所以我們在應用退出時,要將線程銷燬,我們只要在Activity中的,onDestory()方法處理一下就OK了,如下代碼所示:  
@Override    
  protected void onDestroy() {    
    mHandler.removeCallbacks(mRunnable);    
    super.onDestroy();    
  }   
   

3.Bitmap
可以說出現OutOfMemory問題的絕大多數人,都是因爲Bitmap的問題。因爲Bitmap佔用的內存實在是太多了,特別是分辨率大的圖片,如果要顯示多張那問題就更顯著了。


    解決方案:

   第一、及時的銷燬。

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

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

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

private ImageView preview;  

BitmapFactory.Options options = newBitmapFactory.Options();  

options.inSampleSize =2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一  

Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri),null, options); preview.setImageBitmap(bitmap); 

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

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

4.行蹤詭異的Cursor

   Cursor是Android查詢數據後得到的一個管理數據集合的類,正常情況下,如果查詢得到的數據量較小時不會有內存問題,而且虛擬機能夠保證Cusor最終會被釋放掉。

   然而如果Cursor的數據量特表大,特別是如果裏面有Blob信息時,應該保證Cursor佔用的內存被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於編程者手動的將Cursorclose掉

 而且android數據庫中對Cursor資源的是又限制個數的,如果不及時close掉,會導致別的地方無法獲得
    
5.構造Adapter時,沒有使用緩存的convertView 
 以構造ListView的BaseAdapter爲例,在BaseAdapter中提高了方法:
   public View getView(intposition, View convertView, ViewGroup parent)

  AdapterView在使用View會有一個循環的View隊列的,把不顯示的View重新投入使用,所以在convertView不爲空的時候,不要直接創建新的View



小結:

static:引用了大對象如context

線程:切屏時Activity因爲線程引用而沒有如期被銷燬;handler有關,Activity意外終止但線程還在

Bitmap:要及時recycle,降低採樣率

Cursor:要及時關閉

Adapter:沒有使用緩存的convertView

 

四、內存泄漏調試:
(1).內存監測工具 DDMS --> Heap
 用 Heap監測應用進程使用內存情況的步驟如下:
      1.切換到DDMS透視圖,並確認Devices視圖、Heap視圖都是打開的;
      2.正常與手機鏈接成功後,在DDMS的Devices視圖中將會顯示手機設備的序列號,以及設備中正在運行的部分進程信息;
      3.點擊選中想要監測的進程
      4.點擊選中Devices視圖界面中最上方一排圖標中的“Update Heap”圖標;
      5.點擊Heap視圖中的“Cause GC”按鈕;
      6.此時在Heap視圖中就會看到當前選中的進程的內存使用量的詳細情況。
  Heap視圖界面會定時刷新,在對應用的不斷的操作過程中就可以看到內存使用的變化;
 如何才能知道我們的程序是否有內存泄漏的可能性呢。這裏需要注意一個值:Heap視圖中部有一個Type叫做dataobject,即數據對象,也就是我們的程序中大量存在的類類型的對象。在data object一行中有一列是“TotalSize”,其值就是當前進程中所有Java數據對象的內存總量,一般情況下,這個值的大小決定了是否會有內存泄漏。可以這樣判斷:
   a) 不斷的操作當前應用,同時注意觀察dataobject的Total Size值;
   b) 正常情況下TotalSize值都會穩定在一個有限的範圍內,也就是說由於程序中的的代碼良好,沒有造成對象不被垃圾回收的情況,所以說 雖然我們不斷的操作會不斷的生成很多對象,而在虛擬機不斷的進行GC的過程中,這些對象都被回收了,內存佔用量會會落到一個穩定的水平;
    c) 反之如果代碼中存在沒有釋放對象引用的情況,則dataobject的Total Size值在每次GC後不會有明顯的回落,隨着操作次數的增多TotalSize的值會越來越大,

(2)內存分析工具MAT(Memory Analyzer Tool)
  這裏介紹一個極好的內存分析工具 -- Memory AnalyzerTool(MAT)。
  MAT是一個Eclipse插件,同時也有單獨的RCP客戶端。官方下載地址、MAT介紹和詳細的使用教程請參見:www.eclipse.org/mat,在此不進行說明了。另外在MAT安裝後的幫助文檔裏也有完備的使用教程。在此僅舉例說明其使用方法。我自己使用的是MAT的eclipse插件,使用插件要比RCP稍微方便一些。插件安裝成功後,分析步驟(安裝方法有多重,大家隨便)
 
   (a)生成.hprof文件
  
      1.打開eclipse並切換到DDMS
      2.點擊選中想要分析的應用的進程,在Devices視圖上方的一行圖標按鈕中,選中“UpdateHeap”。
      3.當內存你感覺異常的時候,按下“DumpHPROF file”按鈕,這個時候會提示設置hprof文件的保存路徑。
(二) 使用MAT導入.hprof文件
     1.通過/ANDROID_SDK/tools目錄下的hprof-conv.exe工具(使用命令同adb),輸入命令hprof-convxxx.hprofyyy.hprof,其中xxx.hprof爲原始文件,yyy.hprof爲轉換過後的文件。
    2.在Eclipse中點擊Windows->Open Perspective->Other->MemoryAnalysis perspective界面。在MAT中點擊File->Open File,瀏覽並導入剛剛轉換而得到的.hprof文件。
(三) 使用MAT的視圖工具分析內存
  導入.hprof文件以後,MAT會自動解析並生成報告,報告中會列出使用內存過多或者初始化的實例過多的類。

 點擊DominatorTree,並按Package分組,選擇報告中提到的可疑實例的類,在彈出菜單中選擇List objects->Withincoming references。這時會列出所有可疑類,右鍵點擊某一項,並選擇Path to GC Roots ->exclude weak/softreferences,會進一步篩選出跟程序相關的所有有內存泄露的類。據此,可以追蹤到代碼中的某一個產生泄露的類。
  主要是看可疑類的引用是因爲什麼代碼的引用而導致無法釋放的
  總之使用MAT分析內存查找內存泄漏的根本思路,就是找到哪個類的對象的引用沒有被釋放,找到沒有被釋放的原因,也就可以很容易定位代碼中的哪些片段的邏輯有問題了。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章