Android 內(nèi)存泄漏分析心得

轉(zhuǎn)自: QQ空間終端開(kāi)發(fā)團(tuán)隊(duì)

前言


Paste_Image.png

對(duì)于C++來(lái)說(shuō),內(nèi)存泄漏就是new出來(lái)的對(duì)象沒(méi)有delete,俗稱(chēng)野指針罢缸;對(duì)于Java來(lái)說(shuō),就是new出來(lái)的Object放在Heap上無(wú)法被GC回收;本文通過(guò)QQ和Qzone中內(nèi)存泄漏實(shí)例來(lái)講android中內(nèi)存泄漏分析解法和編寫(xiě)代碼應(yīng)注意的事項(xiàng)椭坚。

Java中的內(nèi)存分配

  1. 靜態(tài)儲(chǔ)存區(qū):編譯時(shí)就分配好良价,在程序整個(gè)運(yùn)行期間都存在寝殴。它主要存放靜態(tài)數(shù)據(jù)和常量;
  2. 棧區(qū):當(dāng)方法執(zhí)行時(shí)明垢,會(huì)在棧區(qū)內(nèi)存中創(chuàng)建方法體內(nèi)部的局部變量蚣常,方法結(jié)束后自動(dòng)釋放內(nèi)存;
  3. 堆區(qū):通常存放 new 出來(lái)的對(duì)象痊银。由 Java 垃圾回收器回收抵蚊。
四鐘引用類(lèi)型的介紹

  1. 強(qiáng)引用(StrongReference):JVM 寧可拋出 OOM ,也不會(huì)讓 GC 回收具有強(qiáng)引用的對(duì)象;
  2. 軟引用(SoftReference):只有在內(nèi)存空間不足時(shí)贞绳,才會(huì)被回的對(duì)象谷醉;
  3. 弱引用(WeakReference):在 GC 時(shí),一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象冈闭,不管當(dāng)前內(nèi)存空間足夠與否俱尼,都會(huì)回收它的內(nèi)存;
  4. 虛引用(PhantomReference):任何時(shí)候都可以被GC回收萎攒,當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí)遇八,如果發(fā)現(xiàn)它還有虛引用,就會(huì)在回收對(duì)象的內(nèi)存之前耍休,把這個(gè)虛引用加入到與之關(guān)聯(lián)的引用隊(duì)列中刃永。程序可以通過(guò)判斷引用隊(duì)列中是否存在該對(duì)象的虛引用,來(lái)了解這個(gè)對(duì)象是否將要被回收羹应±康猓可以用來(lái)作為GC回收Object的標(biāo)志。

我們常說(shuō)的內(nèi)存泄漏是指new出來(lái)的Object無(wú)法被GC回收园匹,即為強(qiáng)引用:

Paste_Image.png

內(nèi)存泄漏發(fā)生時(shí)的主要表現(xiàn)為內(nèi)存抖動(dòng)雳刺,可用內(nèi)存慢慢變少:

Paste_Image.png
Andriod中分析內(nèi)存泄漏的工具M(jìn)AT

  1. MAT(Memory Analyzer Tools)是一個(gè) Eclipse 插件,它是一個(gè)快速裸违、功能豐富的JAVA heap分析工具掖桦,它可以幫助我們查找內(nèi)存泄漏和減少內(nèi)存消耗。
  2. MAT 插件的下載地址: www.eclipse.org/mat
  3. MAT 使用方法介紹:
    http://www.cnblogs.com/larack/p/6071209.html
QQ和Qzone內(nèi)存泄漏如何監(jiān)控

Paste_Image.png

QQ和Qzone 的內(nèi)存泄漏采用SNGAPM解決方案供汛,SNGAPM是一個(gè)性能監(jiān)控枪汪、分析的統(tǒng)一解決方案,它從終端收集性能信息怔昨,上報(bào)到一個(gè)后臺(tái)雀久,后臺(tái)將監(jiān)控類(lèi)信息聚合展示為圖表,將分析類(lèi)信息進(jìn)行分析并提單趁舀,通知開(kāi)發(fā)者赖捌;

  1. SNGAPM由App(MagnifierApp)和web server(MagnifierServer)兩部分組成;
  2. MagnifierApp在自動(dòng)內(nèi)存泄漏檢測(cè)中是一個(gè)銜接檢測(cè)組件(LeakInspector)和自動(dòng)化云分析(MagnifierCloud)的中間性平臺(tái)矮烹,它從LeakInspector的內(nèi)存dump自動(dòng)化上傳MagnifierServer越庇;
  3. MagnifierServer后臺(tái)會(huì)定時(shí)提交分析任務(wù)到MagnifierCloud;
  4. MagnifierCloud分析結(jié)束之后會(huì)更新數(shù)據(jù)到magnifier web上奉狈,同時(shí)以bug單形式通知開(kāi)發(fā)者卤唉。

常見(jiàn)的內(nèi)存泄漏案例


case 1. 單例造成的內(nèi)存泄露

單例的靜態(tài)特性導(dǎo)致其生命周期同應(yīng)用一樣長(zhǎng)。

解決方案:

  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;
}    
/**
 * 被點(diǎn)擊的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;
}    
/**
 * 被點(diǎn)擊的view
 */
private WeakReference<View> mScrolledViewWeakRef = null;    
public void setScrolledView(View scrolledView) {
    mScrolledViewWeakRef = new WeakReference<View>(scrolledView);
}
case 2. InnerClass匿名內(nèi)部類(lèi)

在Java中桑驱,非靜態(tài)內(nèi)部類(lèi) 和 匿名類(lèi) 都會(huì)潛在的引用它們所屬的外部類(lèi)竭恬,但是,靜態(tài)內(nèi)部類(lèi)卻不會(huì)熬的。如果這個(gè)非靜態(tài)內(nèi)部類(lèi)實(shí)例做了一些耗時(shí)的操作萍聊,就會(huì)造成外圍對(duì)象不會(huì)被回收,從而導(dǎo)致內(nèi)存泄漏悦析。

解決方案:

  1. 將內(nèi)部類(lèi)變成靜態(tài)內(nèi)部類(lèi);
  2. 如果有強(qiáng)引用Activity中的屬性,則將該屬性的引用方式改為弱引用;
  3. 在業(yè)務(wù)允許的情況下此衅,當(dāng)Activity執(zhí)行onDestory時(shí)强戴,結(jié)束這些耗時(shí)任務(wù);
    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)匿名內(nèi)部類(lè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應(yīng)用程序中通车舶埃可以使用兩種Context對(duì)象:Activity和Application骑歹。當(dāng)類(lèi)或方法需要Context對(duì)象的時(shí)候常見(jiàn)的做法是使用第一個(gè)作為Context參數(shù)。這樣就意味著View對(duì)象對(duì)整個(gè)Activity保持引用墨微,因此也就保持對(duì)Activty的所有的引用道媚。

假設(shè)一個(gè)場(chǎng)景,當(dāng)應(yīng)用程序有個(gè)比較大的Bitmap類(lèi)型的圖片翘县,每次旋轉(zhuǎn)是都重新加載圖片所用的時(shí)間較多最域。為了提高屏幕旋轉(zhuǎn)是Activity的創(chuàng)建速度,最簡(jiǎn)單的方法時(shí)將這個(gè)Bitmap對(duì)象使用Static修飾锈麸。 當(dāng)一個(gè)Drawable綁定在View上镀脂,實(shí)際上這個(gè)View對(duì)象就會(huì)成為這份Drawable的一個(gè)Callback成員變量。而靜態(tài)變量的生命周期要長(zhǎng)于Activity忘伞。導(dǎo)致了當(dāng)旋轉(zhuǎn)屏幕時(shí)薄翅,Activity無(wú)法被回收,而造成內(nèi)存泄露氓奈。

解決方案:

  1. 使用ApplicationContext代替ActivityContext翘魄,因?yàn)锳pplicationContext會(huì)隨著應(yīng)用程序的存在而存在,而不依賴(lài)于activity的生命周期舀奶;
  2. 對(duì)Context的引用不要超過(guò)它本身的生命周期暑竟,慎重的對(duì)Context使用“static”關(guān)鍵字。Context里如果有線程伪节,一定要在onDestroy()里及時(shí)停掉光羞。

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引起的內(nèi)存泄漏

當(dāng)Handler中有延遲的的任務(wù)或是等待執(zhí)行的任務(wù)隊(duì)列過(guò)長(zhǎng),由于消息持有對(duì)Handler的引用怀大,而Handler又持有對(duì)其外部類(lèi)的潛在引用纱兑,這條引用關(guān)系會(huì)一直保持到消息得到處理,而導(dǎo)致了Activity無(wú)法被垃圾回收器回收化借,而導(dǎo)致了內(nèi)存泄露潜慎。

解決方案:

  1. 可以把Handler類(lèi)放在單獨(dú)的類(lèi)文件中,或者使用靜態(tài)內(nèi)部類(lèi)便可以避免泄露;
  2. 如果想在Handler內(nèi)部去調(diào)用所在的Activity,那么可以在handler內(nèi)部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來(lái)達(dá)到斷開(kāi)Handler與Activity之間存在引用關(guān)系的目的。

Solution

@Override
protected void doOnDestroy() {        
    super.doOnDestroy();        
    if (mHandler != null) {
        mHandler.removeCallbacksAndMessages(null);
    }
    mHandler = null;
    mRenderCallback = null;
}
case 5. 注冊(cè)監(jiān)聽(tīng)器的泄漏

系統(tǒng)服務(wù)可以通過(guò)Context.getSystemService 獲取铐炫,它們負(fù)責(zé)執(zhí)行某些后臺(tái)任務(wù)垒手,或者為硬件訪問(wèn)提供接口。如果Context 對(duì)象想要在服務(wù)內(nèi)部的事件發(fā)生時(shí)被通知倒信,那就需要把自己注冊(cè)到服務(wù)的監(jiān)聽(tīng)器中科贬。然而,這會(huì)讓服務(wù)持有Activity 的引用鳖悠,如果在Activity onDestory時(shí)沒(méi)有釋放掉引用就會(huì)內(nèi)存泄漏榜掌。
解決方案:

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

Solution:

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

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

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沒(méi)有close憎账,View沒(méi)有recyle

資源性對(duì)象比如(Cursor,F(xiàn)ile文件等)往往都用了一些緩沖卡辰,我們?cè)诓皇褂玫臅r(shí)候胞皱,應(yīng)該及時(shí)關(guān)閉它們,以便它們的緩沖及時(shí)回收內(nèi)存九妈。它們的緩沖不僅存在于 java虛擬機(jī)內(nèi)反砌,還存在于java虛擬機(jī)外。如果我們僅僅是把它的引用設(shè)置為null,而不關(guān)閉它們允蚣,往往會(huì)造成內(nèi)存泄漏于颖。因?yàn)橛行┵Y源性對(duì)象,比如SQLiteCursor(在析構(gòu)函數(shù)finalize(),如果我們沒(méi)有關(guān)閉它嚷兔,它自己會(huì)調(diào)close()關(guān)閉)森渐,如果我們沒(méi)有關(guān)閉它,系統(tǒng)在回收它時(shí)也會(huì)關(guān)閉它冒晰,但是這樣的效率太低了同衣。因此對(duì)于資源性對(duì)象在不使用的時(shí)候,應(yīng)該調(diào)用它的close()函數(shù)壶运,將其關(guān)閉掉耐齐,然后才置為null. 在我們的程序退出時(shí)一定要確保我們的資源性對(duì)象已經(jīng)關(guān)閉。
Solution:

調(diào)用onRecycled()

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

在View中調(diào)用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. 集合中對(duì)象沒(méi)清理造成的內(nèi)存泄漏

我們通常把一些對(duì)象的引用加入到了集合容器(比如ArrayList)中蒋情,當(dāng)我們不需要該對(duì)象時(shí)埠况,并沒(méi)有把它的引用從集合中清理掉,這樣這個(gè)集合就會(huì)越來(lái)越大棵癣。如果這個(gè)集合是static的話辕翰,那情況就更嚴(yán)重了。
所以要在退出程序之前狈谊,將集合里的東西clear喜命,然后置為null沟沙,再退出程序。

解決方案:

在Activity退出之前壁榕,將集合里的東西clear矛紫,然后置為null,再退出程序牌里。

Solution

private List<EmotionPanelInfo> data;    
public void onDestory() {        
    if (data != null) {
        data.clear();
        data = null;
    }
}
case 8. WebView造成的泄露

當(dāng)我們不要使用WebView對(duì)象時(shí)颊咬,應(yīng)該調(diào)用它的destory()函數(shù)來(lái)銷(xiāo)毀它,并釋放其占用的內(nèi)存牡辽,否則其占用的內(nèi)存長(zhǎng)期也不能被回收贪染,從而造成內(nèi)存泄露。

解決方案:

為webView開(kāi)啟另外一個(gè)進(jìn)程催享,通過(guò)AIDL與主線程進(jìn)行通信,WebView所在的進(jìn)程可以根據(jù)業(yè)務(wù)的需要選擇合適的時(shí)機(jī)進(jìn)行銷(xiāo)毀哟绊,從而達(dá)到內(nèi)存的完整釋放因妙。

case 9. 構(gòu)造Adapter時(shí),沒(méi)有使用緩存的ConvertView

初始時(shí)ListView會(huì)從Adapter中根據(jù)當(dāng)前的屏幕布局實(shí)例化一定數(shù)量的View對(duì)象票髓,同時(shí)ListView會(huì)將這些View對(duì)象 緩存起來(lái)攀涵。
當(dāng)向上滾動(dòng)ListView時(shí),原先位于最上面的List Item的View對(duì)象會(huì)被回收洽沟,然后被用來(lái)構(gòu)造新出現(xiàn)的最下面的List Item以故。
這個(gè)構(gòu)造過(guò)程就是由getView()方法完成的,getView()的第二個(gè)形參View ConvertView就是被緩存起來(lái)的List Item的View對(duì)象(初始化時(shí)緩存中沒(méi)有View對(duì)象則ConvertView是null)裆操。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末怒详,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子踪区,更是在濱河造成了極大的恐慌昆烁,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缎岗,死亡現(xiàn)場(chǎng)離奇詭異静尼,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)传泊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)鼠渺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人眷细,你說(shuō)我怎么就攤上這事拦盹。” “怎么了薪鹦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵掌敬,是天一觀的道長(zhǎng)惯豆。 經(jīng)常有香客問(wèn)我,道長(zhǎng)奔害,這世上最難降的妖魔是什么楷兽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任字旭,我火速辦了婚禮另假,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘图仓。我一直安慰自己雅潭,他們只是感情好揭厚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著扶供,像睡著了一般筛圆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椿浓,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天太援,我揣著相機(jī)與錄音,去河邊找鬼扳碍。 笑死提岔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笋敞。 我是一名探鬼主播碱蒙,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼夯巷!你這毒婦竟也來(lái)了赛惩?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤趁餐,失蹤者是張志新(化名)和其女友劉穎坊秸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體澎怒,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡褒搔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喷面。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片星瘾。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖惧辈,靈堂內(nèi)的尸體忽然破棺而出琳状,到底是詐尸還是另有隱情,我是刑警寧澤盒齿,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布念逞,位于F島的核電站困食,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏翎承。R本人自食惡果不足惜硕盹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叨咖。 院中可真熱鬧瘩例,春花似錦、人聲如沸甸各。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)趣倾。三九已至聘惦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間儒恋,已是汗流浹背部凑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碧浊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓瘟仿,卻偏偏與公主長(zhǎng)得像箱锐,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子劳较,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容