Android內存優化-內存泄漏的幾個場景以及解決方式


轉自:http://blog.csdn.net/a910626/article/details/50849760

一.什么是內存泄漏

Java程序中,如果一個對象沒有利用價值了,正常情況下gc是會對其進行回收的,但是此時仍然有其他引用指向這個活在堆內存中的對象,那么gc就不會認為這個對象是一個垃圾,那么就不會對其進行回收,所以它會一直活在堆內存中占用內存,這就導致了內存泄漏。

總結一下,導致內存泄漏的原因就是有一些我們永遠不會使用的對象,仍然有引用指向它(當然這是在強引用的情況下),那么就不滿足gc回收的條件,從而一直活在堆內存中導致內存泄漏,這樣的對象多了占用大量內存就會導致App發生oom。

舉幾個例子:比如使用EventBus,肯定是要執行register(),那么在Fragment或Activity finish的時候,一定不要忘記執行unregister()方法。

二.內存泄漏的常見場景以及解決方式

1.Activity中的Handler長期持有activity引用導致activity泄漏

public class MainActivity extends AppCompatActivity { private final Handler myHandler = new Handler(){ @Override public void handleMessage(Message msg) { //doSomething } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myHandler.postDelayed(new Runnable() { @Override public void run() { //doSomething } },60*10*1000); } }

由於myHandler延時10分鍾就會發送一條消息,當activity finish之后,延時發送的消息會在主線程的消息隊列中存活10分鍾直到被looper拿到然后給到handler處理。此消息(new Runnable)隱式持有其外部類handler的引用,myHandler又隱式的持有其外部類Activity的引用,直到消息被處理完之后,這個引用都不會被釋放。因此Activity即使finish,但仍然不會被gc回收。 

引用的順序MessageQueue->Message->Runnable->Handler->Activity,從這個引用鏈得到Activity與MessageQueue關聯,所以Activity對象不能被gc回收,從而導致內存泄漏。

解決方式: 
為了解決Handler隱式的持有外部類引用,我們應當將Handler定義在一個新文件或在Activity中使用靜態內部類。因為靜態內部類不會持有外部類的引用,這樣當Activity finish時,Handler不會持有Activity的引用就不會導致Activity內存泄漏。如果需要在Handler內部調用外部Activity的方法,正確的做法是讓Handler持有一個Activity的弱引用(WeakReference),這樣當gc掃描的時候,這個弱引用的對象就會被回收。 
解決了Handler隱式持有外部類Activity引用,Runnable在之前的代碼中作為匿名內部類隱式持有Handler引用,所以我們在Activity內部定義一個靜態變量引用Runnable類,這是因為匿名類的靜態實例不會隱式持有他們外部類的引用。

public class MainActivity extends AppCompatActivity { private final MyHandler mHandler = new MyHandler(this); private static Runnable sRunnable = new Runnable() { @Override public void run() { //doSomething } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mHandler.postDelayed(sRunnable, 60 * 10); this.finish(); } private static class MyHandler extends Handler { private final WeakReference<MainActivity> mActivity; public MyHandler(MainActivity activity) { this.mActivity = new WeakReference<MainActivity>(activity); } @Override public void handleMessage(Message msg) { MainActivity activity = mActivity.get(); if (activity != null) { //doSomething } } } }

或者

我們也可以這樣做: 
在Activity的onDestroy方法中干掉handler中所有的callback和message:

@Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }

2.非靜態匿名內部類造成內存泄漏

Android中最常見的操作就是當有耗時操作的時候我們不能在主線程執行這些操作,否則有可能造成ANR,主線程主要是UI操作的主戰場。 
比如網絡請求或者數據庫查詢這些耗時操作我們需要自己另外開啟線程,在子線程中執行這些耗時操作。當我們需要開啟的子線程比較少的時候,直接new Thread(Runnable)就可以了。如果你經常這樣做的話就說明你沒有注意到有可能會產生內存泄漏的問題。 
如果Activity結束了,而Thread還在跑,同樣會導致Activity內存泄漏,這是因為new Thread作為非靜態內部類對象都會隱式持有一個外部類對象的引用,我們所創建的線程就是Activity中的一個內部類,持有Activity對象的引用,所以當Activity 結束了,而子線程還在跑就會導致Activity內存泄漏。

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); testThread(); } private void testThread() { new Thread(new Runnable() { @Override public void run() { while (true){ SystemClock.sleep(1000); } } }).start(); } }new Thread()是匿名內部類,且非靜態。所以會隱式持有外部類的一個引用,只要非靜態匿名類對象沒有被回收,Activity就不會被回收。

解決方式: 
同樣把Thread定義為靜態的內部類,這樣就不會持有外部類的引用。

3.單例+依賴注入

LeakActivity.java

public class LeakActivity extends AppCompatActivity { private TestManager testManager = TestManager.getInstance(); private MyListener listener=new MyListener() { @Override public void doSomeThing() {} }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); testManager.registerListener(listener); } }

TestManager.java

public class TestManager { private static final TestManager INSTANCE = new TestManager(); private MyListener listener; public static TestManager getInstance() { return INSTANCE; } public void registerListener(MyListener listener) { this.listener = listener; } public void unregisterListener() { listener = null; } } interface MyListener { void doSomeThing(); }

在LeakActivity中TestManager.getInstance()創建對象實例,TestManager中采用單例模式返回一個Testmanager實例變量。 
引用鏈:TestManager->listener->Activity

TestManager中的實例變量是static靜態變量,靜態變量和類的生命周期是一樣的。類加載的時候,靜態變量就被加載,類銷毀時,靜態變量也會隨之銷毀。 
因為INSTANCE是一個單例,所以和app的生命周期是一樣的。當app進程銷毀時,堆內存中的INSTANCE對象才會被釋放,INSTANCE的生命周期非常的長。 
而又可以看到代碼中Activity里面創建了listener非靜態內部類,所以listener就持有外部類Activity的引用。隨着testManager.registerListener(listener)執行,TestManager中的listener就持有Activity中listener對象,由此形成了一個引用鏈。關鍵在於INSTANCE是一個靜態變量,往往Activity finish的時候,INSTANCE還活着,而INSTANCE依然持有Activity的引用,所以造成了Activity內存泄漏。

所以,解決方式:要在Activity的onDestroy()方法中注銷注冊的listener

@Override protected void onDestroy() { testManager.unregisterListener(); super.onDestroy(); }

 

將TestManager中listener與Activity中的listener關聯斷開。

三.總結

出現內存泄露的主要原因是生命周期的不一致造成的:在Android中,長時間運行的任務和Acyivity生命周期進行協調會有點困難,如果你不加以小心的話會導致內存泄漏。 
內存泄漏的主要原因在於一個生命周期長的東西間接引用了一個生命周期短的東西,會造成生命周期短的東西無法被回收。反過來,如果是一個生命周期短的東西引用了一個生命周期長的東西,是不會影響生命周期短的東西被回收的。 
  對象都是有生命周期的,對象的生命周期有的是進程級別的,有的是Activity所在的生命周期,隨Activity消亡;有的是Service所在的生命周期,隨Service消亡。很多情況下判斷對象是否合理存在的一個很重要的理由就是它實際的生命周期是否符合它本來的生命周期。很多Memory Leak的發生,很大程度上都是生命周期的錯配,本來在隨Activity銷毀的對象變成了進程級別的對象,Memory Leak就無法避免了。

四.避免內存泄漏的一些技巧

    1. 使用靜態內部類/匿名類,不要使用非靜態內部類/匿名類.非靜態內部類/匿名類會隱式的持有外部類的引用,外部類就有可能發生泄漏。而靜態內部類/匿名類不會隱式的持有外部類引用,外部類會以正常的方式回收,如果你想在靜態內部類/匿名類中使用外部類的屬性或方法時,可以顯示的持有一個弱引用。
    2. 不要以為Java永遠會幫你清理回收正在運行的threads.在上面的代碼中,我們很容易誤以為當Activity結束銷毀時會幫我們把正在運行的thread也結束回收掉,但事情永遠不是這樣的!Java threads會一直存在,只有當線程運行完成或被殺死掉,線程才會被回收。所以我們應該養成為thread設置退出邏輯條件的習慣。
    3. 適當的考慮下是否應該使用線程.Android應用框架設計了許多的類來簡化執行后台任務,我們可以使用與Activity生命周期相關聯的Loaders來執行簡短的后台查詢任務。如果一個線程不依賴與Activity,我們還可以使用Service來執行后台任務,然后用BroadcastReceiver來向Activity報告結果。另外需要注意的是本文討論的thread同樣使用於AsyncTasks,AsyncTask同樣也是由線程來實現,只不過使用了Java5.0新增並發包中的功能,但同時需要注意的是根據官方文檔所說,AsyncTask適用於執行一些簡短的后台任務。
    4. 頻繁的使用static關鍵字修飾 
      很多初學者非常喜歡用static類static變量,聲明賦值調用都簡單方便。由於static聲明變量的生命周期其實是和APP的生命周期一樣的(進程級別)。大量的使用的話,就會占據內存空間不釋放,積少成多也會造成內存的不斷開銷,直至掛掉。static的合理使用一般用來修飾基本數據類型或者輕量級對象,盡量避免修復集合或者大對象,常用作修飾全局配置項、工具類方法、內部類。
    5. BitMap隱患 
      Bitmap的不當處理極可能造成OOM,絕大多數情況應用程序OOM都是因這個原因出現的。Bitamp位圖是Android中當之無愧的胖子,所以在操作的時候必須小心。 
      及時釋放recycle。由於Dalivk並不會主動的去回收,需要開發者在Bitmap不被使用的時候recycle掉。 
      設置一定的壓縮率。需求允許的話,應該去對BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅只想獲得Bitmap的屬性,其實並不需要根據BItmap的像素去分配內存,只需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。 
      最后建議大家在加載網絡圖片的時候,使用軟引用或者弱引用並進行本地緩存,推薦使用android-universal-imageloader或者xUtils。
    6. 引用地獄 
      Activity中生成的對象原則上是應該在Activity生命周期結束之后就釋放的。Activity對象本身也是,所以應該盡量避免有appliction進程級別的對象來引用Activity級別的對象,如果有的話也應該在Activity結束的時候解引用。如不應用applicationContext在Activity中獲取資源。Service也一樣。
      有的時候我們也會為了程序的效率性能把本來是Activity級里才用的資源提升到進程級別,比如ImageCache,或者其它DataManager等。 
      我只能說,空間和時間是相對的,有的時候需要犧牲時間換取空間,有的時候需要犧牲空間換取時間。內存是空間的存在,性能是時間的存在。完美的程序是在一定條件下的完美。
    7. BroadCastReceiver、Service 解綁 
      綁定廣播和服務,一定要記得在不需要的時候給解綁。
    8. handler 清理 
      在Activity的onDestroy方法中調用 
      handler.removeCallbacksAndMessages(null); 
      取消所有的消息的處理,包括待處理的消息;
    9. Cursor及時關閉 
      在查詢SQLite數據庫時,會返回一個Cursor,當查詢完畢后,及時關閉,這樣就可以把查詢的結果集及時給回收掉。
    10. I/O流 
      I/O流操作完畢,讀寫結束,記得關閉。
    11. 線程 
      線程不再需要繼續執行的時候要記得及時關閉,開啟線程數量不易過多,一般和自己機器內核數一樣最好,推薦開啟線程的時候,使用線程池。線程生命周期要跟activity同步。
    12. 網絡請求也是線程操作的,也應該與activity生命周期同步,在onDestroy的時候cancle掉請求。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM