Android的內存泄漏和調試

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

2、Android的內存溢出 
Android的內存溢出是如何發生的? 
Android的虛擬機是基於寄存器的Dalvik,它的最大堆大小通常是16M,有的機器爲24M。所以咱們所能利用的內存空間是有限的。若是咱們的內存佔用超過了必定的水平就會出現OutOfMemory的錯誤。 
爲何會出現內存不夠用的狀況呢?我想緣由主要有兩個: 
因爲咱們程序的失誤,長期保持某些資源(如Context)的引用,形成內存泄露,資源形成得不到釋放。 
保存了多個耗用內存過大的對象(如Bitmap),形成內存超出限制。 

3、常見的內存泄漏 
1.萬惡的static 
  static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。因此用static修飾的變量,它的生命週期是很長的,若是用它來引用一些資源耗費過多的實例(Context的狀況最多),這時就要謹慎對待了。 

public class ClassName {      
    private static Context mContext;       //省略 
}  
以上的代碼是很危險的,若是將Activity賦值到麼mContext的話。那麼即便該Activity已經onDestroy,可是因爲仍有對象保存它的引用,所以該Activity依然不會被釋放. 

如何纔能有效的避免這種引用的發生呢? 
    第一,應該儘可能避免static成員變量引用資源耗費過多的實例,好比Context。 
    第2、Context儘可能使用Application Context,由於Application的Context的生命週期比較長,引用它不會出現內存泄露的問題。 
    第3、使用WeakReference代替強引用。好比可使用WeakReference<Context> 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函數是一個很費時的操做,當咱們開啓該線程後,將設備的 橫屏變爲了豎屏,通常狀況下當屏幕轉換時會從新建立Activity,按照咱們的想法,老的Activity應該會被銷燬纔對,然而事實上並不是如此。 

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

這種線程致使的內存泄露問題應該如何解決呢? 
    第1、將線程的內部類,改成靜態內部類。 
    第2、在線程內部採用弱引用保存Context引用。 
    
    另外,咱們都知道Hanlder是線程與Activity通訊的橋樑,咱們在開發好多應用中會用到線程,有些人處理不當,會致使當程序結束時,線程並無 被銷燬,而是一直在後臺運行着,當咱們從新啓動應用時,又會從新啓動一個線程,周而復始,你啓動應用次數越多,開啓的線程數就越多,你的機器就會變得越 慢。 
package com.tutor.thread;  
import android.app.Activity;  
import android.os.Bundle;  
import android.os.Handler;  
import android.util.Log;  
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);  
            count++;  
            setTitle("" +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帶給咱們的內存問題? 

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

    第2、設置必定的採樣率。 
    有時候,咱們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只須要記載一個縮小過的圖片,這時候能夠設置必定的採樣率,那麼就能夠大大減少佔用的內存。以下面的代碼: 
private ImageView preview;  
BitmapFactory.Options options = new BitmapFactory.Options();  
options.inSampleSize = 2;//圖片寬高都爲原來的二分之一,即圖片爲原來的四分之一  
Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options);  preview.setImageBitmap(bitmap); 

第3、巧妙的運用軟引用(SoftRefrence) 
    有些時候,咱們使用Bitmap後沒有保留對它的引用,所以就沒法調用Recycle函數。這時候巧妙的運用軟引用,可使Bitmap在內存快不足時獲得有效的釋放。 
   

4.行蹤詭異的Cursor 
    Cursor是Android查詢數據後獲得的一個管理數據集合的類,正常狀況下,若是查詢獲得的數據量較小時不會有內存問題,並且虛擬機可以保證Cusor最終會被釋放掉。 
    然而若是Cursor的數據量特表大,特別是若是裏面有Blob信息時,應該保證Cursor佔用的內存被及時的釋放掉,而不是等待GC來處理。而且Android明顯是傾向於編程者手動的將Cursor close掉 
    
5.構造Adapter時,沒有使用緩存的 convertView 
描述: 
  以構造ListView的BaseAdapter爲例,在BaseAdapter中提升了方法: 
public View getView(int position, 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) 方法。 
示例代碼: 
public View getView(int position, View convertView, ViewGroup parent) { 
  View view = new Xxx(...); 
  ... ... 
  return view; 

修正示例代碼: 
public View getView(int position, View convertView, ViewGroup parent) { 
  View view = null; 
  if (convertView != null) { 
  view = convertView; 
  populate(view, getItem(position)); 
  ... 
  } else { 
  view = new Xxx(...); 
  ... 
  } 
  return view; 


小結: 
static:引用了大對象如context 
線程:切屏時Activity由於線程引用而沒有如期被銷燬;handler有關,Activity意外終止但線程還在 
Bitmap:要及時recycle,下降採樣率 
Cursor:要及時關閉 
Adapter:沒有使用緩存的convertView 

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

(2).內存分析工具 MAT(Memory Analyzer Tool) 
(一) 生成.hprof文件 
(二) 使用MAT導入.hprof文件 
(三) 使用MAT的視圖工具分析內存 android

相關文章
相關標籤/搜索