Android中內存泄露與如何有效避免OOM總結

1、關於OOM與內存泄露的概念

       咱們在Android開發過程當中常常會遇到OOM的錯誤,這是由於咱們在APP中沒有考慮dalvik虛擬機內存消耗的問題。java

       一、什麼是OOM

          OOM:即OutOfMemoery,顧名思義就是指內存溢出了。內存溢出是指APP向系統申請超過最大閥值的內存請求,系統不會再分配多餘的空間,就會形成OOM error。在咱們Android平臺下,多數狀況是出如今圖片不當處理加載的時候。android

          Android系統爲每一個應用程序分配的內存有限,當一個應用中產生的內存泄漏比較多時,就不免會致使應用所須要的內存超過這個系統分配的內存限額,這就形成了內存溢出而致使應用Crash。Android APP的所能申請的最大內存大小是多少,有人說是16MB,有人又說是24MB。其實這些答案都算對,由於Android是開源的操做系統,不一樣的手機廠商實際上是擁有修改這部分權限能力的,因此就形成了不一樣品牌和不一樣系統的手機,對於APP的內存支持也是不同的,不過咱們能夠經過Runtime這個類來獲取當前設備的Android系統爲每一個應用所產生的內存大小。APP並不會爲咱們建立Runtime的實例,Java爲咱們提供了單例獲取的方式Runtime.getRuntime()。經過maxMemory()方法獲取系統可爲APP分配的最大內存,totalMemory()獲取APP當前所分配的內存heap空間大小。算法

 

       二、什麼是內存泄露

 

       Java使用有向圖機制,經過GC自動檢查內存中的對象(何時檢查由虛擬機決定),若是GC發現一個或一組對象爲不可到達狀態,則將該對象從內存中回收。也就是說,一個對象不被任何引用所指向,則該對象會在被GC發現的時候被回收;另外,若是一組對象中只包含互相的引用,而沒有來自它們外部的引用(例若有兩個對象A和B互相持有引用,但沒有任何外部對象持有指向A或B的引用),這仍然屬於不可到達,一樣會被GC回收。數據庫

      在Android程序開發中,當一個對象已經不須要再使用了,本該被回收時,而另一個正在使用的對象持有它的引用從而致使它不能被回收,這就致使本該被回收的對象不能被回收而停留在堆內存中,內存泄漏就產生了。api

      內存泄露的危害:只有一個,那就是虛擬機佔用內存太高,致使OOM(內存溢出),程序出錯。對於Android應用來講,就是你的用戶打開一個Activity,使用完以後關閉它,內存泄露;又打開,又關閉,又泄露;幾回以後,程序佔用內存超過系統限制,FC。緩存

      瞭解了內存泄漏的緣由及影響後,咱們須要作的就是掌握常見的內存泄漏,並在之後的Android程序開發中,儘可能避免它。安全

2、常見的內存泄漏及解決方案

     一、單例形成的內存泄漏

     Android的單例模式很是受開發者的喜好,不過使用的不恰當的話也會形成內存泄漏。網絡

由於單例的靜態特性使得單例的生命週期和應用的生命週期同樣長,這就說明了若是一個對象已經不須要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就致使了內存泄漏。框架

     以下這個典例:異步

public class AppManager {    
    private static AppManager instance;    
    private Context context;    
    private AppManager(Context context) {    
        this.context = context;    
    }    
    public static AppManager getInstance(Context context) {    
        if (instance != null) {    
            instance = new AppManager(context);    
        }    
        return instance;    
    }    
}  

這是一個普通的單例模式,當建立這個單例的時候,因爲須要傳入一個Context,因此

這個Context的生命週期的長短相當重要:

     1)、傳入的是Application的Context:這將沒有任何問題,由於單例的生命週期和Application的同樣長;

     2)、傳入的是Activity的Context:當這個Context所對應的Activity退出時,因爲該Context和Activity的生命週期同樣長(Activity間接繼承於Context),因此當前Activity退出時它的內存並不會被回收,由於單例對象持有該Activity的引用。

     因此正確的單例應該修改成下面這種方式:

public class AppManager {    
    private static AppManager instance;    
    private Context context;    
    private AppManager(Context context) {    
        this.context = context.getApplicationContext();    
    }    
    public static AppManager getInstance(Context context) {    
        if (instance != null) {    
            instance = new AppManager(context);    
        }    
        return instance;    
    }    
} 

這樣無論傳入什麼Context最終將使用Application的Context,而單例的生命週期和應用的同樣長,這樣就防止了內存泄漏。

     二、非靜態內部類建立靜態實例形成的內存泄漏

     在Java 中,非靜態匿名內部類會持有其外部類的隱式引用,若是你沒有考慮過這一點,那麼存儲該引用會致使Activity被保留,而不是被垃圾回收機制回收。Activity對象持有其View層以及相關聯的全部資源文件的引用,換句話說,若是你的內存泄漏發生在Activity中,那麼你將損失大量的內存空間。

     有的時候咱們可能會在啓動頻繁的Activity中,爲了不重複建立相同的數據資源,會出現這種寫法:

public class MainActivity extends AppCompatActivity {    
    private static TestResource mResource = null;    
    @Override    
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);    
        setContentView(R.layout.activity_main);    
        if(mManager == null){    
            mManager = new TestResource();    
        }    
        //...    
    }    
    class TestResource {    
        //...    
    }    
}

這樣就在Activity內部建立了一個非靜態內部類的單例,每次啓動Activity時都會使用該單例的數據,這樣雖然避免了資源的重複建立,不過這種寫法卻會形成內存泄漏,由於非靜態內部類默認會持有外部類的引用,而又使用了該非靜態內部類建立了一個靜態的實例,該實例的生命週期和應用的同樣長,這就致使了該靜態實例一直會持有該Activity的引用,致使Activity的內存資源不能正常回收。正確的作法爲:

      將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,若是須要使用Context,請使用ApplicationContext。

     三、Handler形成的內存泄漏

     Handler的使用形成的內存泄漏問題應該說最爲常見了,平時在處理網絡任務或者封裝一些請求回調等api都應該會藉助Handler來處理,對於Handler的使用代碼編寫一不規範即有可能形成內存泄漏,以下示例:

Handler mHandler = new Handler() {    
    @Override    
    public void handleMessage(Message msg) {    
        mImageView.setImageBitmap(mBitmap);    
    }    
} 

上面是一段簡單的Handler的使用。當使用內部類(包括匿名類)來建立Handler的時候,Handler對象會隱式地持有一個外部類對象(一般是一個Activity)的引用(否則你怎麼可能經過Handler來操做Activity中的View?)。而Handler一般會伴隨着一個耗時的後臺線程(例如從網絡拉取圖片)一塊兒出現,這個後臺線程在任務執行完畢(例如圖片下載完畢)以後,經過消息機制通知Handler,而後Handler把圖片更新到界面。然而,若是用戶在網絡請求過程當中關閉了Activity,正常狀況下,Activity再也不被使用,它就有可能在GC檢查時被回收掉,但因爲這時線程還沒有執行完,而該線程持有Handler的引用(否則它怎麼發消息給Handler?),這個Handler又持有Activity的引用,就致使該Activity沒法被回收(即內存泄露),直到網絡請求結束(例如圖片下載完畢)。另外,若是你執行了Handler的postDelayed()方法:

          //要作的事情,這裏再次調用此Runnable對象,以實現每兩秒實現一次的定時器操做

     handler.postDelayed(this, 2000);  

     該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,那麼在你設定的delay到達以前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,致使你的Activity被持有引用而沒法被回收。

     這種建立Handler的方式會形成內存泄漏,因爲mHandler是Handler的非靜態匿名內部類的實例,因此它持有外部類Activity的引用,咱們知道消息隊列是在Looper中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。

     使用Handler致使內存泄露的解決方法

     方法一:經過程序邏輯來進行保護。

     1).在關閉Activity的時候停掉你的後臺線程。線程停掉了,就至關於切斷了Handler和外部鏈接的線,Activity天然會在合適的時候被回收。

     2).若是你的Handler是被delay的Message持有了引用,那麼使用相應的Handler的removeCallbacks()方法,把消息對象從消息隊列移除就好了。

     方法二:將Handler聲明爲靜態類。

     靜態類不持有外部類的對象,因此你的Activity能夠隨意被回收。代碼以下:

static class MyHandler extends Handler {    
    @Override    
    public void handleMessage(Message msg) {    
        mImageView.setImageBitmap(mBitmap);    
    }    
} 

但其實沒這麼簡單。使用了以上代碼以後,你會發現,因爲Handler再也不持有外部類對象的引用,致使程序不容許你在Handler中操做Activity中的對象了。因此你須要在Handler中增長一個對Activity的弱引用(WeakReference):

static class MyHandler extends Handler {    
    WeakReference<Activity > mActivityReference;    
    MyHandler(Activity activity) {    
        mActivityReference= new WeakReference<Activity>(activity);    
    }    
    @Override    
    public void handleMessage(Message msg) {    
        final Activity activity = mActivityReference.get();    
        if (activity != null) {    
            mImageView.setImageBitmap(mBitmap);    
        }    
    }    
} 

將代碼改成以上形式以後,就算完成了。

     延伸:什麼是WeakReference?

     WeakReference弱引用,與強引用(即咱們常說的引用)相對,它的特色是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某對象,但只要該對象沒有被強引用指向(實際上多數時候還要求沒有軟引用,但此處軟引用的概念能夠忽略),該對象就會在被GC檢查到時回收掉。對於上面的代碼,用戶在關閉Activity以後,就算後臺線程還沒結束,但因爲僅有一條來自Handler的弱引用指向Activity,因此GC仍然會在檢查的時候把Activity回收掉。這樣,內存泄露的問題就不會出現了。

     四、線程形成的內存泄漏

     對於線程形成的內存泄漏,也是平時比較常見的,以下這兩個示例可能每一個人都這樣寫過:

//——————test1    
        new AsyncTask<Void, Void, Void>() {    
            @Override    
            protected Void doInBackground(Void... params) {    
                SystemClock.sleep(10000);    
                return null;    
            }    
        }.execute();    
//——————test2    
        new Thread(new Runnable() {    
            @Override    
            public void run() {    
                SystemClock.sleep(10000);    
            }    
        }).start();  

上面的異步任務和Runnable都是一個匿名內部類,所以它們對當前Activity都有一個隱式引用。若是Activity在銷燬以前,任務還未完成, 那麼將致使Activity的內存資源沒法回收,形成內存泄漏。正確的作法仍是使用靜態內部類的方式,以下:

static class MyAsyncTask extends AsyncTask<Void, Void, Void> {    
        private WeakReference<Context> weakReference;    
      
        public MyAsyncTask(Context context) {    
            weakReference = new WeakReference<>(context);    
        }    
      
        @Override    
        protected Void doInBackground(Void... params) {    
            SystemClock.sleep(10000);    
            return null;    
        }    
      
        @Override    
        protected void onPostExecute(Void aVoid) {    
            super.onPostExecute(aVoid);    
            MainActivity activity = (MainActivity) weakReference.get();    
            if (activity != null) {    
                //...    
            }    
        }    
    }    
    static class MyRunnable implements Runnable{    
        @Override    
        public void run() {    
            SystemClock.sleep(10000);    
        }    
    }    
//——————    
    new Thread(new MyRunnable()).start();    
    new MyAsyncTask(this).execute(); 

經過上面的代碼,新線程不再會持有一個外部Activity 的隱式引用,並且該Activity也會在配置改變後被回收。這樣就避免了Activity的內存資源泄漏,固然在Activity銷燬時候也應該取消相應的任務AsyncTask::cancel(),避免任務在後臺執行浪費資源。

     若是咱們線程作的是一個無線循環更新UI的操做,以下代碼:

 
private static class MyThread extends Thread {    
        @Override    
        public void run() {    
          while (true) {    
            SystemClock.sleep(1000);    
          }    
        }    
      }  

這樣雖然避免了Activity沒法銷燬致使的內存泄露,可是這個線程卻發生了內存泄露。在Java中線程是垃圾回收機制的根源,也就是說,在運行系統中DVM虛擬機總會使硬件持有全部運行狀態的進程的引用,結果致使處於運行狀態的線程將永遠不會被回收。所以,你必須爲你的後臺線程實現銷燬邏輯!下面是一種解決辦法:

private static class MyThread extends Thread {    
        private boolean mRunning = false;    
    
        @Override    
        public void run() {    
          mRunning = true;    
          while (mRunning) {    
            SystemClock.sleep(1000);    
          }    
        }    
    
        public void close() {    
          mRunning = false;    
        }    
      }   

咱們在Activity退出時,能夠在 onDestroy()方法中顯示調用mThread.close();以此來結束該線程,這就避免了線程的內存泄漏問題。

     五、資源對象沒關閉形成的內存泄漏

     資源性對象好比(Cursor,File文件等)每每都用了一些緩衝,咱們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。它們的緩衝不只存在於java虛擬機內,還存在於java虛擬機外。若是咱們僅僅是把它的引用設置爲null,而不關閉它們,每每會形成內存泄漏。由於有些資源性對象,好比SQLiteCursor(在析構函數finalize(),若是咱們沒有關閉它,它本身會調close()關閉),若是咱們沒有關閉它,系統在回收它時也會關閉它,可是這樣的效率過低了。所以對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,而後才置爲null.在咱們的程序退出時必定要確保咱們的資源性對象已經關閉。

     程序中常常會進行查詢數據庫的操做,可是常常會有使用完畢Cursor後沒有關閉的狀況。若是咱們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操做的狀況下才會復現內存問題,這樣就會給之後的測試和問題排查帶來困難和風險。

     示例代碼:

Cursor cursor = getContentResolver().query(uri...);      
if (cursor.moveToNext()) {      
  ... ...        
} 

修正示例代碼:

Cursor cursor = null;      
try {      
  cursor = getContentResolver().query(uri...);      
  if (cursor != null &&cursor.moveToNext()) {      
      ... ...        
  }      
} finally {      
  if (cursor != null) {      
      try {        
          cursor.close();      
      } catch (Exception e) {      
          //ignore this       
      }      
   }      
}  

六、Bitmap沒有回收致使的內存溢出

     Bitmap的不當處理很可能形成OOM,絕大多數狀況都是因這個緣由出現的。Bitamp位圖是Android中當之無愧的胖小子,因此在操做的時候固然是十分的當心了。因爲Dalivk並不會主動的去回收,須要開發者在Bitmap不被使用的時候recycle掉。使用的過程當中,及時釋放是很是重要的。同時若是需求容許,也能夠去BItmap進行必定的縮放,經過BitmapFactory.Options的inSampleSize屬性進行控制。若是僅僅只想得到Bitmap的屬性,其實並不須要根據BItmap的像素去分配內存,只需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。最後建議你們在加載網絡圖片的時候,使用軟引用或者弱引用並進行本地緩存,推薦使用android-universal-imageloader或者xUtils,牛人出品,必屬精品。

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

     以構造ListView的BaseAdapter爲例,在BaseAdapter中提供了方法:

public View getView(int position, ViewconvertView, 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 --> voidaddScrapView(View scrap)方法。

     示例代碼:

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

3、預防OOM的幾點建議

     Android開發過程當中,在 Activity的生命週期裏協調耗時任務可能會很困難,你一不當心就會致使內存泄漏問題。下面是一些小提示,能幫助你預防內存泄漏問題的發生:

     一、合理使用static:

     每個非靜態內部類實例都會持有一個外部類的引用,若該引用是Activity 的引用,那麼該Activity在被銷燬時將沒法被回收。若是你的靜態內部類須要一個相關Activity的引用以確保功能可以正常運行,那麼你得確保你在對象中使用的是一個Activity的弱引用,不然你的Activity將會發生意外的內存泄漏。可是要注意,當此類在全局多處用到時在這樣幹,由於static聲明變量的生命週期實際上是和APP的生命週期同樣的,有點相似與Application。若是大量的使用的話,就會佔據內存空間不釋放,聚沙成塔也會形成內存的不斷開銷,直至掛掉。static的合理使用通常用來修飾基本數據類型或者輕量級對象,儘可能避免修復集合或者大對象,經常使用做修飾全局配置項、工具類方法、內部類。

     二、善用SoftReference/WeakReference/LruCache

     Java、Android中有沒有這樣一種機制呢,當內存吃緊或者GC掃過的狀況下,就能及時把一些內存佔用給釋放掉,從而分配給須要分配的地方。答案是確定的,java爲咱們提供了兩個解決方案。若是對內存的開銷比較關注的APP,能夠考慮使用WeakReference,當GC回收掃過這塊內存區域時就會回收;若是不是那麼關注的話,可使用SoftReference,它會在內存申請不足的狀況下自動釋放,一樣也能解決OOM問題。同時Android自3.0之後也推出了LruCache類,使用LRU算法就釋放內存,同樣的能解決OOM,若是兼容3.0一下的版本,請導入v4包。關於第二條的無關引用的問題,咱們傳參能夠考慮使用WeakReference包裝一下。

     三、謹慎handler

     在處理異步操做的時候,handler + thread是個不錯的選擇。可是相信在使用handler的時候,你們都會遇到警告的情形,這個就是lint爲開發者的提醒。handler運行於UI線程,不斷處理來自MessageQueue的消息,若是handler還有消息須要處理可是Activity頁面已經結束的狀況下,Activity的引用其實並不會被回收,這就形成了內存泄漏。解決方案,一是在Activity的onDestroy方法中調handler.removeCallbacksAndMessages(null);取消全部的消息的處理,包括待處理的消息;二是聲明handler的內部類爲static。

     四、不要總想着Java 的垃圾回收機制會幫你解決全部內存回收問題

     就像上面的示例,咱們覺得垃圾回收機制會幫咱們將不須要使用的內存回收,例如:咱們須要結束一個Activity,那麼它的實例和相關的線程都該被回收。但現實並不會像咱們劇本那樣走。Java線程會一直存活,直到他們都被顯式關閉,抑或是其進程被Android系統殺死。因此,爲你的後臺線程實現銷燬邏輯是你在使用線程時必須時刻銘記的細節,此外,你在設計銷燬邏輯時要根據Activity的生命週期去設計,避免出現Bug。

     考慮你是否真的須要使用線程。Android應用的框架層爲咱們提供了不少便於開發者執行後臺操做的類。例如:咱們可使用Loader 代替在Activity 的生命週期中用線程經過注入執行短暫的異步後臺查詢操做,考慮用Service將結構通知給UI的BroadcastReceiver。最後,記住,這篇博文中對線程進行的討論一樣適用於AsyncTask(由於AsyncTask使用ExecutorService執行它的任務)。然而,雖然說ExecutorService只能在短暫操做(文檔說最多幾秒)中被使用,那麼這些方法致使的Activity內存泄漏應該永遠不會發生。

     五、ListView和GridView的item緩存

     對於移動設備,尤爲硬件良莠不齊的android生態,頁面的繪製實際上是很耗時的,findViewById也是蠻慢的。因此不重用View,在有列表的時候就尤其顯著了,常常會出現滑動很卡的現象,因此咱們要善於重複利用建立好的控件。這裏主要注意兩點:

     1)convertView重用

     ListView中的每個Item顯示都須要Adapter調用一次getView()的方法,這個方法會傳入一個convertView的參數,這個方法返回的View就是這個Item顯示的View。Android提供了一個叫作Recycler(反覆循環)的構件,就是當ListView的Item從滾出屏幕視角以外,對應Item的View會被緩存到Recycler中,相應的會從生成一個Item,而此時調用的getView中的convertView參數就是滾出屏幕的緩存Item的View,因此說若是能重用這個convertView,就會大大改善性能。

     2)使用ViewHolder重用

     咱們都知道在getView()方法中的操做是這樣的:先從xml中建立view對象(inflate操做,咱們採用了重用convertView方法優化),而後在這個view去findViewById,找到每個item的子View的控件對象,如:ImageView、TextView等。這裏的findViewById操做是一個樹查找過程,也是一個耗時的操做,因此這裏也須要優化,就是使用ViewHolder,把每個item的子View控件對象都放在Holder中,當第一次建立convertView對象時,便把這些item的子View控件對象findViewById實例化出來並保存到ViewHolder對象中。而後用convertView的setTag將viewHolder對象設置到Tag中, 當之後加載ListView的item時即可以直接從Tag中取出複用ViewHolder對象中的,不須要再findViewById找item的子控件對象了。這樣便大大提升了性能。

     不過Android5.L爲咱們提供了RecyclerView,RecyclerView是經典的ListView的進化與昇華,它比ListView更加靈活,但也所以引入了必定的複雜性。最新的v7支持包新添加了RecyclerView。RecyclerView提供了一種插拔式的體驗,高度的解耦,異常的靈活,經過設置它提供的不一樣LayoutManager,ItemDecoration , ItemAnimator實現使人瞠目的效果。並且RecyclerView內部爲咱們處理了item緩存,因此用着效率更高,更安全,感興趣的讀者能夠了解一下。

相關文章
相關標籤/搜索