Android常見內(nèi)存泄漏匯總

目錄:
一魏颓、內(nèi)存泄漏介紹
二愁溜、常見內(nèi)存泄漏場(chǎng)景
??????1.單例導(dǎo)致內(nèi)存泄露
??????2.靜態(tài)變量導(dǎo)致內(nèi)存泄漏
??????3.非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄露
??????4.未取消注冊(cè)或回調(diào)導(dǎo)致內(nèi)存泄露
??????5.Timer和TimerTask導(dǎo)致內(nèi)存泄露
??????6.集合中的對(duì)象未清理造成內(nèi)存泄露
??????7.資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露
??????8.屬性動(dòng)畫造成內(nèi)存泄露
??????9.WebView造成內(nèi)存泄露
三掷伙、總結(jié)

歡迎評(píng)論留言蹬叭,文章持續(xù)更新優(yōu)化

一行疏、內(nèi)存泄漏介紹

不少人認(rèn)為JAVA程序弱匪,因?yàn)橛欣厥諜C(jī)制柠逞,應(yīng)該沒有內(nèi)存泄露昧狮。我們已經(jīng)知道了,如果某個(gè)對(duì)象板壮,從根節(jié)點(diǎn)可到達(dá)逗鸣,也就是存在從根節(jié)點(diǎn)到該對(duì)象的引用鏈,那么該對(duì)象是不會(huì)被 GC 回收的绰精。如果說這個(gè)對(duì)象已經(jīng)不會(huì)再被使用到了撒璧,是無用的,我們依然持有他的引用的話笨使,就會(huì)造成內(nèi)存泄漏卿樱,例如 一個(gè)長(zhǎng)期在后臺(tái)運(yùn)行的線程持有 Activity 的引用,這個(gè)時(shí) 候 Activity 執(zhí)行了 onDestroy 方法硫椰,那么這個(gè) Activity 就是從根節(jié)點(diǎn)可到達(dá)并且無用的對(duì)象繁调, 這個(gè) Activity 對(duì)象就是泄漏的對(duì)象,給這個(gè)對(duì)象分配的內(nèi)存將無法被回收靶草。如果我們的java運(yùn)行很久,而這種內(nèi)存泄露不斷的發(fā)生蹄胰,最后就沒內(nèi)存可用了。

Android的一個(gè)應(yīng)用程序的內(nèi)存泄露對(duì)別的應(yīng)用程序影響不大爱致。為了能夠使得Android應(yīng)用程序安全且快速的運(yùn)行烤送,Android的每個(gè)應(yīng)用程序都會(huì)使用一個(gè)專有的Dalvik虛擬機(jī)實(shí)例來運(yùn)行,它是由Zygote服務(wù)進(jìn)程孵化出來的糠悯,也就是說每個(gè)應(yīng)用程序都是在屬于自己的進(jìn)程中運(yùn)行的帮坚。Android為不同類型的進(jìn)程分配了不同的內(nèi)存使用上限,如果程序在運(yùn)行過程中出現(xiàn)了內(nèi)存泄漏的而造成應(yīng)用進(jìn)程使用的內(nèi)存超過了這個(gè)上限互艾,則會(huì)被系統(tǒng)視為內(nèi)存泄漏试和,從而被kill掉,這使得僅僅自己的進(jìn)程被kill掉纫普,而不會(huì)影響其他進(jìn)程(如果是system_process等系統(tǒng)進(jìn)程出問題的話阅悍,則會(huì)引起系統(tǒng)重啟)好渠。

二、常見內(nèi)存泄漏場(chǎng)景

1.單例導(dǎo)致內(nèi)存泄露

單例模式在Android開發(fā)中會(huì)經(jīng)常用到节视,但是如果使用不當(dāng)就會(huì)導(dǎo)致內(nèi)存泄露拳锚。因?yàn)閱卫撵o態(tài)特性使得它的生命周期同應(yīng)用的生命周期一樣長(zhǎng),如果一個(gè)對(duì)象已經(jīng)沒有用處了寻行,但是單例還持有它的引用霍掺,那么在整個(gè)應(yīng)用程序的生命周期它都不能正常被回收,從而導(dǎo)致內(nèi)存泄露拌蜘。

public class AppSettings {

    private static volatile AppSettings singleton;
    private Context mContext;
    private AppSettings(Context context) {
        this.mContext = context;
    }

    public static AppSettings getInstance(Context context) {
        if (singleton == null) {
            synchronized (AppSettings.class) {
                if (singleton == null) {
                    singleton = new AppSettings(context);
                }
            }
        }
        return singleton;
    }
}

像上面代碼中這樣的單例杆烁,如果我們?cè)谡{(diào)用getInstance(Context context)方法的時(shí)候傳入的context參數(shù)是Activity、Service等上下文简卧,就會(huì)導(dǎo)致內(nèi)存泄露兔魂。 以Activity為例,當(dāng)我們啟動(dòng)一個(gè)Activity举娩,并調(diào)用getInstance(Context context)方法去獲取AppSettings的單例析校,傳入Activity.this作為context,這樣AppSettings類的單例sInstance就持有了Activity的引用铜涉,當(dāng)我們退出Activity時(shí)勺良,該Activity就沒有用了,但是因?yàn)閟Intance作為靜態(tài)單例(在應(yīng)用程序的整個(gè)生命周期中存在)會(huì)繼續(xù)持有這個(gè)Activity的引用骄噪,導(dǎo)致這個(gè)Activity對(duì)象無法被回收釋放尚困,這就造成了內(nèi)存泄露。
為了避免這樣單例導(dǎo)致內(nèi)存泄露链蕊,我們可以將context參數(shù)改為全局的上下文:

private AppSettings(Context context) {
        this.mContext = context.getApplicationContext();
}

2.靜態(tài)變量導(dǎo)致內(nèi)存泄漏

靜態(tài)變量存儲(chǔ)在方法區(qū)事甜,它的生命周期從類加載開始,到整個(gè)進(jìn)程結(jié)束滔韵。一旦靜態(tài)變量初始化后逻谦,它所持有的引用只有等到進(jìn)程結(jié)束才會(huì)釋放。 比如下面這樣的情況陪蜻,在Activity中為了避免重復(fù)的創(chuàng)建info邦马,將sInfo作為靜態(tài)變量:

public class MainActivity2 extends AppCompatActivity {

    public static Info sInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        sInfo = new Info(this);
    }

    class Info {

        private Context mContext;

        public Info(Context context) {
            this.mContext = context;
        }
    }
}

Info作為Activity的靜態(tài)成員,并且持有Activity的引用宴卖,但是sInfo作為靜態(tài)變量滋将,生命周期肯定比Activity長(zhǎng)。所以當(dāng)Activity退出后症昏,sInfo仍然引用了Activity随闽,Activity不能被回收,這就導(dǎo)致了內(nèi)存泄露肝谭。

在Android開發(fā)中掘宪,靜態(tài)持有很多時(shí)候都有可能因?yàn)槠涫褂玫纳芷诓灰恢露鴮?dǎo)致內(nèi)存泄露蛾扇,所以我們?cè)谛陆o態(tài)持有的變量的時(shí)候需要多考慮一下各個(gè)成員之間的引用關(guān)系,并且盡量少地使用靜態(tài)持有的變量魏滚,以避免發(fā)生內(nèi)存泄露镀首。當(dāng)然,我們也可以在適當(dāng)?shù)臅r(shí)候講靜態(tài)量重置為null鼠次,使其不再持有引用蘑斧,這樣也可以避免內(nèi)存泄露。

3.非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄露

非靜態(tài)內(nèi)部類(包括匿名內(nèi)部類)默認(rèn)就會(huì)持有外部類的引用须眷,當(dāng)非靜態(tài)內(nèi)部類對(duì)象的生命周期比外部類對(duì)象的生命周期長(zhǎng)時(shí),就會(huì)導(dǎo)致內(nèi)存泄露沟突。非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄露在Android開發(fā)中有一種典型的場(chǎng)景就是使用Handler花颗,很多開發(fā)者在使用Handler是這樣寫的:

public class MainActivity2 extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        start();
    }

    private void start() {
        Message message = Message.obtain();
        message.what = 1;
        mHandler.sendMessage(message);
    }


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 1) {
                //doNothing
            }
        }
    };
}

也許有人會(huì)說,mHandler并未作為靜態(tài)變量持有Activity引用惠拭,生命周期可能不會(huì)比Activity長(zhǎng)扩劝,應(yīng)該不一定會(huì)導(dǎo)致內(nèi)存泄露呢,顯然不是這樣的职辅! 熟悉Handler消息機(jī)制的都知道棒呛,mHandler會(huì)作為成員變量保存在發(fā)送的消息msg中,即msg持有mHandler的引用域携,而mHandler是Activity的非靜態(tài)內(nèi)部類實(shí)例簇秒,即mHandler持有Activity的引用,那么我們就可以理解為msg間接持有Activity的引用秀鞭。msg被發(fā)送后先放到消息隊(duì)列MessageQueue中趋观,然后等待Looper的輪詢處理(MessageQueue和Looper都是與線程相關(guān)聯(lián)的,MessageQueue是Looper引用的成員變量锋边,而Looper是保存在ThreadLocal中的)皱坛。那么當(dāng)Activity退出后,msg可能仍然存在于消息對(duì)列MessageQueue中未處理或者正在處理豆巨,那么這樣就會(huì)導(dǎo)致Activity無法被回收剩辟,以致發(fā)生Activity的內(nèi)存泄露。

通常在Android開發(fā)中如果要使用內(nèi)部類往扔,但又要規(guī)避內(nèi)存泄露贩猎,一般都會(huì)采用靜態(tài)內(nèi)部類+弱引用的方式。

MyHandler mHandler;

public static class MyHandler extends Handler {

        private WeakReference<Activity> mActivityWeakReference;

        public MyHandler(Activity activity) {
            mActivityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

        }
}

mHandler通過弱引用的方式持有Activity萍膛,當(dāng)GC執(zhí)行垃圾回收時(shí)融欧,遇到Activity就會(huì)回收并釋放所占據(jù)的內(nèi)存單元。這樣就不會(huì)發(fā)生內(nèi)存泄露了卦羡。 上面的做法確實(shí)避免了Activity導(dǎo)致的內(nèi)存泄露噪馏,發(fā)送的msg不再已經(jīng)沒有持有Activity的引用了麦到,但是msg還是有可能存在消息隊(duì)列MessageQueue中,所以更好的是在Activity銷毀時(shí)就將mHandler的回調(diào)和發(fā)送的消息給移除掉欠肾。

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

非靜態(tài)內(nèi)部類造成內(nèi)存泄露還有一種情況就是使用Thread或者AsyncTask瓶颠。要避免內(nèi)存泄露的話還是需要像上面Handler一樣使用靜態(tài)內(nèi)部類+弱應(yīng)用的方式(代碼就不列了,參考上面Hanlder的正確寫法)刺桃。

4.未取消注冊(cè)或回調(diào)導(dǎo)致內(nèi)存泄露

比如我們?cè)贏ctivity中注冊(cè)廣播粹淋,如果在Activity銷毀后不取消注冊(cè),那么這個(gè)剛播會(huì)一直存在系統(tǒng)中瑟慈,同上面所說的非靜態(tài)內(nèi)部類一樣持有Activity引用桃移,導(dǎo)致內(nèi)存泄露。因此注冊(cè)廣播后在Activity銷毀后一定要取消注冊(cè)葛碧。 在注冊(cè)觀察則模式的時(shí)候借杰,如果不及時(shí)取消也會(huì)造成內(nèi)存泄露。比如使用Retrofit+RxJava注冊(cè)網(wǎng)絡(luò)請(qǐng)求的觀察者回調(diào)进泼,同樣作為匿名內(nèi)部類持有外部引用蔗衡,所以需要記得在不用或者銷毀的時(shí)候取消注冊(cè)。

5.Timer和TimerTask導(dǎo)致內(nèi)存泄露

Timer和TimerTask在Android中通常會(huì)被用來做一些計(jì)時(shí)或循環(huán)任務(wù)乳绕,比如實(shí)現(xiàn)無限輪播的ViewPager:

private void stopTimer(){
        if(mTimer!=null){
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if(mTimerTask!=null){
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

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

當(dāng)我們Activity銷毀的時(shí)绞惦,有可能Timer還在繼續(xù)等待執(zhí)行TimerTask,它持有Activity的引用不能被回收洋措,因此當(dāng)我們Activity銷毀的時(shí)候要立即cancel掉Timer和TimerTask济蝉,以避免發(fā)生內(nèi)存泄漏。

6.集合中的對(duì)象未清理造成內(nèi)存泄露

這個(gè)比較好理解菠发,如果一個(gè)對(duì)象放入到ArrayList堆生、HashMap等集合中,這個(gè)集合就會(huì)持有該對(duì)象的引用雷酪。當(dāng)我們不再需要這個(gè)對(duì)象時(shí)淑仆,也并沒有將它從集合中移除,這樣只要集合還在使用(而此對(duì)象已經(jīng)無用了)哥力,這個(gè)對(duì)象就造成了內(nèi)存泄露蔗怠。并且如果集合被靜態(tài)引用的話,集合里面那些沒有用的對(duì)象更會(huì)造成內(nèi)存泄露了吩跋。所以在使用集合時(shí)要及時(shí)將不用的對(duì)象從集合remove寞射,或者clear集合,以避免內(nèi)存泄漏锌钮。

7.資源未關(guān)閉或釋放導(dǎo)致內(nèi)存泄露

在使用IO桥温、File流或者Sqlite、Cursor等資源時(shí)要及時(shí)關(guān)閉。這些資源在進(jìn)行讀寫操作時(shí)通常都使用了緩沖赚楚,如果不及時(shí)關(guān)閉愕鼓,這些緩沖對(duì)象就會(huì)一直被占用而得不到釋放扰她,以致發(fā)生內(nèi)存泄露。因此我們?cè)诓恍枰褂盟鼈兊臅r(shí)候就及時(shí)關(guān)閉投剥,以便緩沖能及時(shí)得到釋放沈善,從而避免內(nèi)存泄露伯诬。

8.屬性動(dòng)畫造成內(nèi)存泄露

動(dòng)畫同樣是一個(gè)耗時(shí)任務(wù)澳腹,比如在Activity中啟動(dòng)了屬性動(dòng)畫(ObjectAnimator)织盼,但是在銷毀的時(shí)候,沒有調(diào)用cancle方法酱塔,雖然我們看不到動(dòng)畫了沥邻,但是這個(gè)動(dòng)畫依然會(huì)不斷地播放下去,動(dòng)畫引用所在的控件羊娃,所在的控件引用Activity唐全,這就造成Activity無法正常釋放。因此同樣要在Activity銷毀的時(shí)候cancel掉屬性動(dòng)畫迁沫,避免發(fā)生內(nèi)存泄漏。

9.WebView造成內(nèi)存泄露

關(guān)于WebView的內(nèi)存泄露捌蚊,因?yàn)閃ebView在加載網(wǎng)頁后會(huì)長(zhǎng)期占用內(nèi)存而不能被釋放集畅,因此我們?cè)贏ctivity銷毀后要調(diào)用它的destory()方法來銷毀它以釋放內(nèi)存。另外在查閱WebView內(nèi)存泄露相關(guān)資料時(shí)看到這種情況: Webview下面的Callback持有Activity引用缅糟,造成Webview內(nèi)存無法釋放挺智,即使是調(diào)用了Webview.destory()等方法都無法解決問題(Android5.1之后)。 最終的解決方案是:在銷毀WebView之前需要先將WebView從父容器中移除窗宦,然后再銷毀WebView赦颇。

三、總結(jié)

  1. 對(duì)于生命周期比Activity長(zhǎng)的對(duì)象(單例)赴涵,要避免直接引用Activity的context媒怯,可以考慮使用ApplicationContext,靜態(tài)變量不使用時(shí)及時(shí)置空髓窜;

  2. Handler持有的引用最好使用弱引用扇苞,在Activity被釋放的時(shí)候要記得清空Message,取消Handler對(duì)象的Runnable寄纵;

  3. 非靜態(tài)內(nèi)部類鳖敷、非靜態(tài)匿名內(nèi)部類會(huì)自動(dòng)持有外部類的引用,為避免內(nèi)存泄露程拭,可以考慮把內(nèi)部類聲明為靜態(tài)的定踱;

  4. 廣播接收器、EventBus等的使用過程中恃鞋,注冊(cè)/反注冊(cè)應(yīng)該成對(duì)使用崖媚,但凡有注冊(cè)的都應(yīng)該有反注冊(cè)亦歉;

  5. 不再使用的資源對(duì)象Cursor、File至扰、Bitmap等要記住正確關(guān)閉鳍徽;

  6. 集合里面的東西有加入就應(yīng)該對(duì)應(yīng)有相應(yīng)的刪除

7.屬性動(dòng)畫及時(shí)取消敢课,注意webview內(nèi)存泄漏問題阶祭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市直秆,隨后出現(xiàn)的幾起案子濒募,更是在濱河造成了極大的恐慌,老刑警劉巖圾结,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑰剃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡筝野,警方通過查閱死者的電腦和手機(jī)晌姚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來歇竟,“玉大人挥唠,你說我怎么就攤上這事』酪椋” “怎么了宝磨?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)盅安。 經(jīng)常有香客問我唤锉,道長(zhǎng),這世上最難降的妖魔是什么别瞭? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任窿祥,我火速辦了婚禮,結(jié)果婚禮上蝙寨,老公的妹妹穿的比我還像新娘壁肋。我一直安慰自己,他們只是感情好籽慢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布浸遗。 她就那樣靜靜地躺著,像睡著了一般箱亿。 火紅的嫁衣襯著肌膚如雪跛锌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音髓帽,去河邊找鬼菠赚。 笑死,一個(gè)胖子當(dāng)著我的面吹牛郑藏,可吹牛的內(nèi)容都是我干的衡查。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼必盖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拌牲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起歌粥,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤塌忽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后失驶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體土居,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年嬉探,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了擦耀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涩堤,死狀恐怖眷蜓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情定躏,我是刑警寧澤账磺,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布芹敌,位于F島的核電站痊远,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏氏捞。R本人自食惡果不足惜碧聪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望液茎。 院中可真熱鬧逞姿,春花似錦、人聲如沸捆等。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栋烤。三九已至谒养,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間明郭,已是汗流浹背买窟。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來泰國打工丰泊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人始绍。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓瞳购,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親亏推。 傳聞我的和親對(duì)象是個(gè)殘疾皇子学赛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356