1. 程式人生 > 其它 >Android開發中應該避免的記憶體洩露

Android開發中應該避免的記憶體洩露

一、背景和目的:

目前許多開發人員在Android開發過程中,較少關注實現細節和記憶體使用,容易會造成記憶體洩露,導致程式OOM。

本文會通過程式碼向大家介紹在Android開發過程中常見的記憶體洩露。

二、常見的記憶體洩露程式碼

1、使用Handler****造成的記憶體問題

在Android開發過程中,Handler是比較常用的,通過Handler傳送Message與主執行緒進行通訊,Message傳送之後是儲存在MessageQueue中的,有些Message並不是馬上被處理的,在Message中存在一個Target,是Handler的一個引用,如果Message在Handler中的存在時間過長,會導致Handler無法被回收。如果Handler非靜態,則會導致相關引用的Activity或者Service不會回收,所以在處理Hanlder之類的內部類的時候,應該要將Handler定義為靜態內部類,同樣在使用HandlerThread的時候也需要注意,我們來看看程式碼:

public class MainActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        HandlerThread mThread = new HandlerThread("threadTask",Process.THREAD_PROIORITY_BACKGROUND);
        mThread.start();
        Handler mHander = new Handler(mThread.getLooper());
        //TODO...
    }
}

這個程式碼存在洩漏問題,因為HandlerThread內部會不斷的迴圈執行,它不會自己結束,執行緒的生命週期超過了activity生命週期,當橫豎屏切換,HandlerThread執行緒的數量會隨著activity重建次數的增加而增加。

我們應該在onDestroy時將執行緒停止掉:mThread.getLooper().quit();

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

2、使用非靜態內部類的靜態例項

public class MainActivity extends AppCompatActivity{

    public class OtherClass{
    }

    private static OtherClass sInstance = null;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(sInstance == null){
           sInstance = new OtherClass();
        }
    }
}

上面的程式碼中的sInstance例項型別為靜態例項,在第一個MainActivityact例項建立時,sInstance會獲得並一直持有activity的引用。當MainAcitivity銷燬後重建,因為sInstance持有activity的引用,所以activity是無法被GC回收的,程序中會存在2個MainActivity例項(activity和重建後的MainActivity例項),這個activity物件就是一個無用的但一直佔用記憶體的物件,即無法回收的垃圾物件。所以,對於lauchMode不是singleInstance的Activity,應該避免在activity裡面例項化其非靜態內部類的靜態例項。

3、在Activity****中使用靜態成員

public class MainActivity extends AppCompatActivity{

    private static Drawable sBackground = null;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView label = new TextView(this);
        if(sBackground == null){
           sBackground = getDrawable(R.mipmap.ic_launcher);
        }
        label.setBackgroundDrawable(sBackground);
    }
}

由於用靜態成員sBackground 快取了drawable物件,所以activity載入速度會加快,但是這樣做是錯誤的。因為它會導致activity銷燬後無法被系統回收。label.setBackgroundDrawable函式呼叫會將label賦值給sBackground的成員變數。上面程式碼意味著:sBackground(GC Root)會持有TextView物件,而TextView持有Activiy物件。所以導致Activity物件無法被系統回收。

以上2個例子的記憶體洩漏都是因為Activity的引用的生命週期超越了activity物件的生命週期。也就是常說的Context洩漏,想要避免context相關的記憶體洩漏,需要注意以下幾點:

l 不要對activity的context長期引用(activity的引用的生存週期應該和activity的生命週期相同)

l 在可以使用application的context的情況下,儘可能使用application的context來替代和activity相關的context

l 如果一個acitivity的非靜態內部類的生命週期不受控制,那麼我們就應該避免這樣使用。

4、註冊某個物件後未登出

註冊廣播接收器、註冊觀察者等等,比如: 在呼叫registerReceiver後,若未呼叫unregisterReceiver,它會導致BroadcastReceiver不會被unregister而導致記憶體洩露,我們經常會看到類似下面的程式碼:

registerReceiver(new BroadcastReceiver(){

    @Override
    public void onReceive(Context context,Intent intent){
    //TODO...
    }

},filter);

5、集合中物件沒清理造成的記憶體洩露

我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,如果物件不斷增大,達到一定的值的時候程式就會OOM

6、資源物件沒關閉造成的記憶體洩露

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

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

寫程式碼時,經常會有人忘記呼叫close, 或者因為程式碼邏輯問題狀況導致close未被呼叫。

錯誤的程式碼:

try{
    Cursor c = queryCursor();
    int a = c.getInt(2);
    c.close();
}catch(Exception ex){

}

修正後的程式碼:

Cursor c = null;
try{
    c = queryCursor();
    int a = c.getInt(2);
    c.close();
}catch(Exception ex){

}finally{
    if(c!=null){
       c.close();
    }
}

7、一些不良程式碼成記憶體壓力

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

7.1 Bitmap 使用不當

一、需要及時的銷燬。

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

二、需要設定一定的取樣率。

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

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; //圖片寬高都為原來的二分之一,圖片為原來的額四分之一
Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);

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

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

SoftReference<Bitmap> bitmap_ref = new SoftReference<Bitmap>(BitmapFactory.decodeStream(is));
if(bitmap_ref.get()!=null){
   bitmap_ref.get().recycle(); 
}

7.2 使用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

public addScrapView (View scrap ,int position)

錯誤的程式碼:

public View getView (int position , View convertView,ViewGroup parent){
    View  view = new TextView(this);
    return view;
}

修正示例程式碼:

public View getView (int position , View convertView,ViewGroup parent){
    View view = null;
    if(convertView !=null){
        view =(View)convertView.getTag();
    } else {
        view = new TextView(this);
        convertView.setTag(view);
    }
    return view;
}

7.3適當的使用物件池

不要在經常呼叫的方法中建立物件,每次new之後都丟棄,尤其是忌諱在迴圈中建立物件。在android support v4包中包含Pools類,其實就是物件池,使用方法也比較簡單,具體可以參考下面的MyPools這個類。

public class MyPools{
    private static final Pools.SynchronizedPool<MyPools> sPool = new Pools.SynchronizedPool<MyPools>(10);

    public static MyPools obtain(){ 
      MyPools instance = sPool.acquire();
      return (instance != null)?instance :new MyPools();
    }

    public void recycle(){
      sPool.release(this);
    }

}