1. 程式人生 > >關於記憶體洩漏總結可能最全的文章

關於記憶體洩漏總結可能最全的文章

轉自: QQ空間終端開發團隊

前言


對於C++來說,記憶體洩漏就是new出來的物件沒有delete,俗稱野指標;對於Java來說,就是new出來的Object 放在Heap上無法被GC回收;本文通過QQ和Qzone中記憶體洩漏例項來講android中記憶體洩漏分析解法和編寫程式碼應注意的事項。

Java 中的記憶體分配

  1. 靜態儲存區:編譯時就分配好,在程式整個執行期間都存在。它主要存放靜態資料和常量;

  2. 棧區:當方法執行時,會在棧區記憶體中建立方法體內部的區域性變數,方法結束後自動釋放記憶體;

  3. 堆區:通常存放 new 出來的物件。由 Java 垃圾回收器回收。

四種引用型別的介紹

  1. 強引用(StrongReference):JVM 寧可丟擲 OOM ,也不會讓 GC 回收具有強引用的物件;

  2. 軟引用(SoftReference):只有在記憶體空間不足時,才會被回的物件;

  3. 弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體;

  4. 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否存在該物件的虛引用,來了解這個物件是否將要被回收。可以用來作為GC回收Object的標誌。

我們常說的記憶體洩漏是指new出來的Object無法被GC回收,即為強引用:

記憶體洩漏發生時的主要表現為記憶體抖動,可用記憶體慢慢變少:

Andriod中分析記憶體洩漏的工具MAT

  1. MAT(Memory Analyzer Tools)是一個 Eclipse 外掛,它是一個快速、功能豐富的JAVA heap分析工具,它可以幫助我們查詢記憶體洩漏和減少記憶體消耗。

  2. MAT 外掛的下載地址: www.eclipse.org/mat

  3. MAT 使用方法介紹:

    http://www.cnblogs.com/larack/p/6071209.html

QQ和Qzone記憶體洩漏如何監控


QQ和Qzone 的記憶體洩漏採用

SNGAPM解決方案,SNGAPM是一個性能監控、分析的統一解決方案,它從終端收集效能資訊,上報到一個後臺,後臺將監控類資訊聚合展示為圖表,將分析類資訊進行分析並提單,通知開發者;

  1. SNGAPM由App(MagnifierApp)和

    web server(MagnifierServer)兩部分組成;

  2. MagnifierApp在自動記憶體洩漏檢測中是一個銜接檢測元件(LeakInspector)和自動化雲分析(MagnifierCloud)的中間性平臺,它從LeakInspector的記憶體dump自動化上傳MagnifierServer;

  3. MagnifierServer後臺會定時提交分析任務到MagnifierCloud;

  4. MagnifierCloud分析結束之後會更新資料到magnifier web上,同時以bug單形式通知開發者。

常見的記憶體洩漏案例

case 1. 單例造成的記憶體洩露

單例的靜態特性導致其生命週期同應用一樣長。

解決方案:

  1. 將該屬性的引用方式改為弱引用;

  2. 如果傳入Context,使用ApplicationContext;

example:

洩漏程式碼片段

privatestatic ScrollHelper mInstance;    
privateScrollHelper() { }    
publicstatic ScrollHelper getInstance() {
   
if (mInstance == null) {
     
synchronized (ScrollHelper.class) {  
           
if (mInstance == null) {                mInstance = new ScrollHelper();            }        }    }        
   
return mInstance; }    
/** * 被點選的view */
private View mScrolledView = null;    
publicvoidsetScrolledView(View scrolledView) {    mScrolledView = scrolledView; }

Solution:使用WeakReference

private static ScrollHelper mInstance;    
private ScrollHelper() { }    
public static ScrollHelper getInstance() {        
   if (mInstance == null) {            
       synchronized (ScrollHelper.class) {                
           if (mInstance == null) {                mInstance = new ScrollHelper();            }        }    }        
       
   return mInstance; }    
/** * 被點選的view */
private WeakReference<View> mScrolledViewWeakRef = null;    
public void setScrolledView(View scrolledView) {    mScrolledViewWeakRef = new WeakReference<View>(scrolledView); }

case 2. InnerClass匿名內部類

在Java中,非靜態內部類 和 匿名類 都會潛在的引用它們所屬的外部類,但是,靜態內部類卻不會。如果這個非靜態內部類例項做了一些耗時的操作,就會造成外圍物件不會被回收,從而導致記憶體洩漏。

解決方案:

  1. 將內部類變成靜態內部類;

  2. 如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用;

  3. 在業務允許的情況下,當Activity執行onDestory時,結束這些耗時任務;

example:

public class LeakAct extends Activity {  
   @Override
   protected void onCreate(Bundle savedInstanceState) {    
       super.onCreate(savedInstanceState);        setContentView(R.layout.aty_leak);        test();
   }
   //這兒發生洩漏    
   public
void test() {    
       new Thread(new Runnable() {      
           @Override            public void run() {        
               while (true) {          
                   try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }).start();    }
}

Solution:

public class LeakAct extends Activity {  
   @Override    protected void onCreate(Bundle savedInstanceState) {    
       super.onCreate(savedInstanceState);        setContentView(R.layout.aty_leak);        test();    }  
   //加上static,變成靜態匿名內部類    public static void test() {    
       new Thread(new Runnable() {    
           @Override            public void run() {        
               while (true) {          
                   try {                        Thread.sleep(1000);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }).start();    } }

case 3. Activity Context 的不正確使用

在Android應用程式中通常可以使用兩種Context物件:Activity和Application。當類或方法需要Context物件的時候常見的做法是使用第一個作為Context引數。這樣就意味著View物件對整個Activity保持引用,因此也就保持對Activty的所有的引用。

假設一個場景,當應用程式有個比較大的Bitmap型別的圖片,每次旋轉是都重新載入圖片所用的時間較多。為了提高螢幕旋轉是Activity的建立速度,最簡單的方法時將這個Bitmap物件使用Static修飾。 當一個Drawable繫結在View上,實際上這個View物件就會成為這份Drawable的一個Callback成員變數。而靜態變數的生命週期要長於Activity。導致了當旋轉螢幕時,Activity無法被回收,而造成記憶體洩露。

解決方案:

  1. 使用ApplicationContext代替ActivityContext,因為ApplicationContext會隨著應用程式的存在而存在,而不依賴於activity的生命週期;

  2. 對Context的引用不要超過它本身的生命週期,慎重的對Context使用“static”關鍵字。Context裡如果有執行緒,一定要在onDestroy()裡及時停掉。

example:

private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {  
   super.onCreate(state);    TextView label = new TextView(this);    label.setText("Leaks are bad");  
   if (sBackground == null) {        sBackground = getDrawable(R.drawable.large_bitmap);    }    label.setBackgroundDrawable(sBackground);    setContentView(label); }

Solution:

private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {  
   super.onCreate(state);    TextView label = new TextView(this);    label.setText("Leaks are bad");  
   if (sBackground == null) {        sBackground = getApplicationContext().getDrawable(R.drawable.large_bitmap);    }    label.setBackgroundDrawable(sBackground);    setContentView(label); }

case 4. Handler引起的記憶體洩漏

當Handler中有延遲的的任務或是等待執行的任務佇列過長,由於訊息持有對Handler的引用,而Handler又持有對其外部類的潛在引用,這條引用關係會一直保持到訊息得到處理,而導致了Activity無法被垃圾回收器回收,而導致了記憶體洩露。

解決方案:

  1. 可以把Handler類放在單獨的類檔案中,或者使用靜態內部類便可以避免洩露;

  2. 如果想在Handler內部去呼叫所在的Activity,那麼可以在handler內部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關係的目的。

Solution

@Override
protected void doOnDestroy() {        
   super.doOnDestroy();        
   if (mHandler != null) {        mHandler.removeCallbacksAndMessages(null);    }    mHandler = null;    mRenderCallback = null; }

case 5. 註冊監聽器的洩漏

系統服務可以通過Context.getSystemService 獲取,它們負責執行某些後臺任務,或者為硬體訪問提供介面。如果Context 物件想要在服務內部的事件發生時被通知,那就需要把自己註冊到服務的監聽器中。然而,這會讓服務持有Activity 的引用,如果在Activity onDestory時沒有釋放掉引用就會記憶體洩漏。

解決方案:

  1. 使用ApplicationContext代替ActivityContext;

  2. 在Activity執行onDestory時,呼叫反註冊;

mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);

Solution:

mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);

下面是容易造成記憶體洩漏的系統服務:

InputMethodManager imm = (InputMethodManager) context.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);

Solution:

protected void onDetachedFromWindow() {        
   if (this.mActionShell != null) {
       this.mActionShell.setOnClickListener((OnAreaClickListener)null);    }        
   if (this.mButtonShell != null) {
       this.mButtonShell.setOnClickListener((OnAreaClickListener)null);    }        
   if (this.mCountShell != this.mCountShell) {
       this.mCountShell.setOnClickListener((OnAreaClickListener)null);    }        
   super.onDetachedFromWindow(); }

case 6. Cursor,Stream沒有close,View沒有recyle

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

Solution:

呼叫onRecycled()

@Override
public void onRecycled() {     reset();    mSinglePicArea.onRecycled(); }

在View中呼叫reset()

publicvoidreset() {
if (mHasRecyled) {            
       return;    } ...    SubAreaShell.recycle(mActionBtnShell);    mActionBtnShell = null; ...    mIsDoingAvatartRedPocketAnim = false;        
   if (mAvatarArea != null) {            mAvatarArea.reset();    }        
   if (mNickNameArea != null) {        mNickNameArea.reset();    } }

case 7. 集合中物件沒清理造成的記憶體洩漏

我們通常把一些物件的引用加入到了集合容器(比如ArrayList)中,當我們不需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
所以要在退出程式之前,將集合裡的東西clear,然後置為null,再退出程式。

解決方案:

在Activity退出之前,將集合裡的東西clear,然後置為null,再退出程式。

Solution

private List<EmotionPanelInfo> data;    
public void onDestory() {        
   if (data != null) {        data.clear();        data = null;    } }

case 8. WebView造成的洩露

當我們不要使用WebView物件時,應該呼叫它的destory()函式來銷燬它,並釋放其佔用的記憶體,否則其佔用的記憶體長期也不能被回收,從而造成記憶體洩露。

解決方案:

為webView開啟另外一個程序,通過AIDL與主執行緒進行通訊,WebView所在的程序可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。

case 9. 構造Adapter時,沒有使用快取的ConvertView

初始時ListView會從Adapter中根據當前的屏幕布局例項化一定數量的View物件,同時ListView會將這些View物件 快取起來。
當向上滾動ListView時,原先位於最上面的List Item的View物件會被回收,然後被用來構造新出現的最下面的List Item。
這個構造過程就是由getView()方法完成的,getView()的第二個形參View ConvertView就是被快取起來的List Item的View物件(初始化時快取中沒有View物件則ConvertView是null)。