1. 程式人生 > >Android中記憶體洩漏與OOM避免措施總結

Android中記憶體洩漏與OOM避免措施總結

文章部落格地址:http://blog.csdn.net/gjnm820/article/details/51579080
一、關於OOM與記憶體洩露的概念
我們在Android開發過程中經常會遇到OOM的錯誤,這是因為我們在APP中沒有考慮dalvik虛擬機器記憶體消耗的問題。
1、什麼是OOM
OOM:即OutOfMemoery,顧名思義就是指記憶體溢位了。記憶體溢位是指APP向系統申請超過最大閥值的記憶體請求,系統不會再分配多餘的空間,就會造成OOM error。在我們Android平臺下,多數情況是出現在圖片不當處理載入的時候。
Android系統為每個應用程式分配的記憶體有限,當一個應用中產生的記憶體洩漏比較多時,就難免會導致應用所需要的記憶體超過這個系統分配的記憶體限額,這就造成了記憶體溢位而導致應用Crash。Android APP的所能申請的最大記憶體大小是多少,有人說是16MB,有人又說是24MB。其實這些答案都算對,因為Android是開源的作業系統,不同的手機廠商其實是擁有修改這部分許可權能力的,所以就造成了不同品牌和不同系統的手機,對於APP的記憶體支援也是不一樣的,不過我們可以通過Runtime這個類來獲取當前裝置的Android系統為每個應用所產生的記憶體大小。APP並不會為我們建立Runtime的例項,Java為我們提供了單例獲取的方式Runtime.getRuntime()。通過maxMemory()方法獲取系統可為APP分配的最大記憶體,totalMemory()獲取APP當前所分配的記憶體heap空間大小。
2、什麼是記憶體洩露


Java使用有向圖機制,通過GC自動檢查記憶體中的物件(什麼時候檢查由虛擬機器決定),如果GC發現一個或一組物件為不可到達狀態,則將該物件從記憶體中回收。也就是說,一個物件不被任何引用所指向,則該物件會在被GC發現的時候被回收;另外,如果一組物件中只包含互相的引用,而沒有來自它們外部的引用(例如有兩個物件A和B互相持有引用,但沒有任何外部物件持有指向A或B的引用),這仍然屬於不可到達,同樣會被GC回收。
在Android程式開發中,當一個物件已經不需要再使用了,本該被回收時,而另外一個正在使用的物件持有它的引用從而導致它不能被回收,這就導致本該被回收的物件不能被回收而停留在堆記憶體中,記憶體洩漏就產生了。
記憶體洩露的危害:只有一個,那就是虛擬機器佔用記憶體過高,導致OOM(記憶體溢位),程式出錯。對於Android應用來說,就是你的使用者開啟一個Activity,使用完之後關閉它,記憶體洩露;又開啟,又關閉,又洩露;幾次之後,程式佔用記憶體超過系統限制,FC。
瞭解了記憶體洩漏的原因及影響後,我們需要做的就是掌握常見的記憶體洩漏,並在以後的Android程式開發中,儘量避免它。
二、常見的記憶體洩漏及解決方案

1、單例造成的記憶體洩漏
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,而單例的生命週期和應用的一樣長,這樣就防止了記憶體洩漏。
2、非靜態內部類建立靜態例項造成的記憶體洩漏
在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。
3、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回收掉。這樣,記憶體洩露的問題就不會出現了。
4、執行緒造成的記憶體洩漏
對於執行緒造成的記憶體洩漏,也是平時比較常見的,如下這兩個示例可能每個人都這樣寫過:

//——————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();以此來結束該執行緒,這就避免了執行緒的記憶體洩漏問題。
5、資源物件沒關閉造成的記憶體洩漏
資源性物件比如(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     
      }    
   }    
}

6、Bitmap沒有回收導致的記憶體溢位
Bitmap的不當處理極可能造成OOM,絕大多數情況都是因這個原因出現的。Bitamp點陣圖是Android中當之無愧的胖小子,所以在操作的時候當然是十分的小心了。由於Dalivk並不會主動的去回收,需要開發者在Bitmap不被使用的時候recycle掉。使用的過程中,及時釋放是非常重要的。同時如果需求允許,也可以去BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅只想獲得Bitmap的屬性,其實並不需要根據BItmap的畫素去分配記憶體,只需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。最後建議大家在載入網路圖片的時候,使用軟引用或者弱引用並進行本地快取,推薦使用android-universal-imageloader或者xUtils,牛人出品,必屬精品。

7、構造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;    
}

三、預防OOM的幾點建議
Android開發過程中,在 Activity的生命週期裡協調耗時任務可能會很困難,你一不小心就會導致記憶體洩漏問題。下面是一些小提示,能幫助你預防記憶體洩漏問題的發生:
1、合理使用static:
每一個非靜態內部類例項都會持有一個外部類的引用,若該引用是Activity 的引用,那麼該Activity在被銷燬時將無法被回收。如果你的靜態內部類需要一個相關Activity的引用以確保功能能夠正常執行,那麼你得確保你在物件中使用的是一個Activity的弱引用,否則你的Activity將會發生意外的記憶體洩漏。但是要注意,當此類在全域性多處用到時在這樣幹,因為static宣告變數的生命週期其實是和APP的生命週期一樣的,有點類似與Application。如果大量的使用的話,就會佔據記憶體空間不釋放,積少成多也會造成記憶體的不斷開銷,直至掛掉。static的合理使用一般用來修飾基本資料型別或者輕量級物件,儘量避免修復集合或者大物件,常用作修飾全域性配置項、工具類方法、內部類。
2、善用SoftReference/WeakReference/LruCache
Java、Android中有沒有這樣一種機制呢,當記憶體吃緊或者GC掃過的情況下,就能及時把一些記憶體佔用給釋放掉,從而分配給需要分配的地方。答案是肯定的,java為我們提供了兩個解決方案。如果對記憶體的開銷比較關注的APP,可以考慮使用WeakReference,當GC回收掃過這塊記憶體區域時就會回收;如果不是那麼關注的話,可以使用SoftReference,它會在記憶體申請不足的情況下自動釋放,同樣也能解決OOM問題。同時Android自3.0以後也推出了LruCache類,使用LRU演算法就釋放記憶體,一樣的能解決OOM,如果相容3.0一下的版本,請匯入v4包。關於第二條的無關引用的問題,我們傳參可以考慮使用WeakReference包裝一下。
3、謹慎handler
在處理非同步操作的時候,handler + thread是個不錯的選擇。但是相信在使用handler的時候,大家都會遇到警告的情形,這個就是lint為開發者的提醒。handler運行於UI執行緒,不斷處理來自MessageQueue的訊息,如果handler還有訊息需要處理但是Activity頁面已經結束的情況下,Activity的引用其實並不會被回收,這就造成了記憶體洩漏。解決方案,一是在Activity的onDestroy方法中調handler.removeCallbacksAndMessages(null);取消所有的訊息的處理,包括待處理的訊息;二是宣告handler的內部類為static。
4、不要總想著Java 的垃圾回收機制會幫你解決所有記憶體回收問題
就像上面的示例,我們以為垃圾回收機制會幫我們將不需要使用的記憶體回收,例如:我們需要結束一個Activity,那麼它的例項和相關的執行緒都該被回收。但現實並不會像我們劇本那樣走。Java執行緒會一直存活,直到他們都被顯式關閉,抑或是其程序被Android系統殺死。所以,為你的後臺執行緒實現銷燬邏輯是你在使用執行緒時必須時刻銘記的細節,此外,你在設計銷燬邏輯時要根據Activity的生命週期去設計,避免出現Bug。
考慮你是否真的需要使用執行緒。Android應用的框架層為我們提供了很多便於開發者執行後臺操作的類。例如:我們可以使用Loader 代替在Activity 的生命週期中用執行緒通過注入執行短暫的非同步後臺查詢操作,考慮用Service將結構通知給UI的BroadcastReceiver。最後,記住,這篇博文中對執行緒進行的討論同樣適用於AsyncTask(因為AsyncTask使用ExecutorService執行它的任務)。然而,雖說ExecutorService只能在短暫操作(文件說最多幾秒)中被使用,那麼這些方法導致的Activity記憶體洩漏應該永遠不會發生。
5、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快取,所以用著效率更高,更安全,感興趣的讀者可以瞭解一下。
文章部落格地址:http://blog.csdn.net/gjnm820/article/details/51579080