Android 內存泄漏分析心得

轉自: QQ空間終端開發(fā)團隊

前言


Paste_Image.png

對于C++來說,內存泄漏就是new出來的對象沒有delete,俗稱野指針;對于Java來說,就是new出來的Object放在Heap上無法被GC回收;本文通過QQ和Qzone中內存泄漏實例來講android中內存泄漏分析解法和編寫代碼應注意的事項。

Java中的內存分配

  1. 靜態(tài)儲存區(qū):編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態(tài)數據和常量;
  2. 棧區(qū):當方法執(zhí)行時,會在棧區(qū)內存中創(chuàng)建方法體內部的局部變量,方法結束后自動釋放內存;
  3. 堆區(qū):通常存放 new 出來的對象。由 Java 垃圾回收器回收。
四鐘引用類型的介紹

  1. 強引用(StrongReference):JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象;
  2. 軟引用(SoftReference):只有在內存空間不足時,才會被回的對象;
  3. 弱引用(WeakReference):在 GC 時,一旦發(fā)現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存;
  4. 虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個對象時,如果發(fā)現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收??梢杂脕碜鳛镚C回收Object的標志。

我們常說的內存泄漏是指new出來的Object無法被GC回收,即為強引用:

Paste_Image.png

內存泄漏發(fā)生時的主要表現為內存抖動,可用內存慢慢變少:

Paste_Image.png
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內存泄漏如何監(jiān)控

Paste_Image.png

QQ和Qzone 的內存泄漏采用SNGAPM解決方案,SNGAPM是一個性能監(jiān)控、分析的統(tǒng)一解決方案,它從終端收集性能信息,上報到一個后臺,后臺將監(jiān)控類信息聚合展示為圖表,將分析類信息進行分析并提單,通知開發(fā)者;

  1. SNGAPM由App(MagnifierApp)和web server(MagnifierServer)兩部分組成;
  2. MagnifierApp在自動內存泄漏檢測中是一個銜接檢測組件(LeakInspector)和自動化云分析(MagnifierCloud)的中間性平臺,它從LeakInspector的內存dump自動化上傳MagnifierServer;
  3. MagnifierServer后臺會定時提交分析任務到MagnifierCloud;
  4. MagnifierCloud分析結束之后會更新數據到magnifier web上,同時以bug單形式通知開發(fā)者。

常見的內存泄漏案例


case 1. 單例造成的內存泄露

單例的靜態(tài)特性導致其生命周期同應用一樣長。

解決方案:

  1. 將該屬性的引用方式改為弱引用;
  2. 如果傳入Context,使用ApplicationContext;

example:

泄漏代碼片段

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 View mScrolledView = null;    
public void setScrolledView(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中,非靜態(tài)內部類 和 匿名類 都會潛在的引用它們所屬的外部類,但是,靜態(tài)內部類卻不會。如果這個非靜態(tài)內部類實例做了一些耗時的操作,就會造成外圍對象不會被回收,從而導致內存泄漏。

解決方案:

  1. 將內部類變成靜態(tài)內部類;
  2. 如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用;
  3. 在業(yè)務允許的情況下,當Activity執(zhí)行onDestory時,結束這些耗時任務;
    example:
public class LeakAct extends Activity {  
    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.aty_leak);
        test();
    } 
    //這兒發(fā)生泄漏    
    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,變成靜態(tài)匿名內部類
    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應用程序中通??梢允褂脙煞NContext對象:Activity和Application。當類或方法需要Context對象的時候常見的做法是使用第一個作為Context參數。這樣就意味著View對象對整個Activity保持引用,因此也就保持對Activty的所有的引用。

假設一個場景,當應用程序有個比較大的Bitmap類型的圖片,每次旋轉是都重新加載圖片所用的時間較多。為了提高屏幕旋轉是Activity的創(chuàng)建速度,最簡單的方法時將這個Bitmap對象使用Static修飾。 當一個Drawable綁定在View上,實際上這個View對象就會成為這份Drawable的一個Callback成員變量。而靜態(tài)變量的生命周期要長于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中有延遲的的任務或是等待執(zhí)行的任務隊列過長,由于消息持有對Handler的引用,而Handler又持有對其外部類的潛在引用,這條引用關系會一直保持到消息得到處理,而導致了Activity無法被垃圾回收器回收,而導致了內存泄露。

解決方案:

  1. 可以把Handler類放在單獨的類文件中,或者使用靜態(tài)內部類便可以避免泄露;
  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. 注冊監(jiān)聽器的泄漏

系統(tǒng)服務可以通過Context.getSystemService 獲取,它們負責執(zhí)行某些后臺任務,或者為硬件訪問提供接口。如果Context 對象想要在服務內部的事件發(fā)生時被通知,那就需要把自己注冊到服務的監(jiān)聽器中。然而,這會讓服務持有Activity 的引用,如果在Activity onDestory時沒有釋放掉引用就會內存泄漏。
解決方案:

  1. 使用ApplicationContext代替ActivityContext;
  2. 在Activity執(zhí)行onDestory時,調用反注冊;
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);

Solution:

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

下面是容易造成內存泄漏的系統(tǒng)服務:

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()關閉),如果我們沒有關閉它,系統(tǒng)在回收它時也會關閉它,但是這樣的效率太低了。因此對于資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然后才置為null. 在我們的程序退出時一定要確保我們的資源性對象已經關閉。
Solution:

調用onRecycled()

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

在View中調用reset()

public void reset() {
    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所在的進程可以根據業(yè)務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

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)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容