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

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

       我們在Android開發過程中經常會遇到OOM的錯誤,這是因爲我們在APP中沒有考慮dalvik虛擬機內存消耗的問題。

       1、什麼是OOM

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

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

       2、什麼是內存泄露

       Java使用有向圖機制,通過GC自動檢查內存中的對象(什麼時候檢查由虛擬機決定),如果GC發現一個或一組對象爲不可到達狀態,則將該對象從內存中回收。也就是說,一個對象不被任何引用所指向,則該對象會在被GC發現的時候被回收;另外,如果一組對象中只包含互相的引用,而沒有來自它們外部的引用(例如有兩個對象AB互相持有引用,但沒有任何外部對象持有指向AB的引用),這仍然屬於不可到達,同樣會被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)、傳入的是ApplicationContext:這將沒有任何問題,因爲單例的生命週期和Application的一樣長;

     2)、傳入的是ActivityContext:當這個Context所對應的Activity退出時,由於該ContextActivity的生命週期一樣長(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最終將使用ApplicationContext,而單例的生命週期和應用的一樣長,這樣就防止了內存泄漏。

     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

     3Handler造成的內存泄漏

     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無法被回收(即內存泄露),直到網絡請求結束(例如圖片下載完畢)。另外,如果你執行了HandlerpostDelayed()方法:

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

     handler.postDelayed(this, 2000);  

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

     這種創建Handler的方式會造成內存泄漏,由於mHandlerHandler的非靜態匿名內部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在Looper中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導致該Activity的內存資源無法及時回收,引發內存泄漏。

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

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

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

     2).如果你的Handler是被delayMessage持有了引用,那麼使用相應的HandlerremoveCallbacks()方法,把消息對象從消息隊列移除就行了。

     方法二:將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、資源對象沒關閉造成的內存泄漏

     資源性對象比如(CursorFile文件等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收內存。它們的緩衝不僅存在於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     
      }    
   }    
}

     6Bitmap沒有回收導致的內存溢出

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

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

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

public View getView(int position, ViewconvertView, ViewGroup parent)

     來向ListView提供每一個item所需要的view對象。初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的view對象,同時ListView會將這些view對象緩存起來。當向上滾動ListView時,原先位於最上面的list itemview對象會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參View convertView就是被緩存起來的list itemview對象(初始化時緩存中沒有view對象則convertViewnull)。由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新實例化一個View對象的話,即浪費資源也浪費時間,也會使得內存佔用越來越大。ListView回收list itemview對象的過程可以查看:

     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

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

     3、謹慎handler

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

     4、不要總想着Java 的垃圾回收機制會幫你解決所有內存回收問題

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

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

     5ListViewGridViewitem緩存

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

     1)convertView重用

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

     2)使用ViewHolder重用

     我們都知道在getView()方法中的操作是這樣的:先從xml中創建view對象(inflate操作,我們採用了重用convertView方法優化),然後在這個viewfindViewById,找到每一個item的子View的控件對象,如:ImageViewTextView等。這裏的findViewById操作是一個樹查找過程,也是一個耗時的操作,所以這裏也需要優化,就是使用ViewHolder,把每一個item的子View控件對象都放在Holder中,當第一次創建convertView對象時,便把這些item的子View控件對象findViewById實例化出來並保存到ViewHolder對象中。然後用convertViewsetTagviewHolder對象設置到Tag中, 當以後加載ListViewitem時便可以直接從Tag中取出複用ViewHolder對象中的,不需要再findViewByIditem的子控件對象了。這樣便大大提高了性能。

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


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