Android性能調(diào)優(yōu)篇之內(nèi)存泄露

開篇廢話

通過我之前的兩篇文章

Android性能調(diào)優(yōu)篇之探索JVM內(nèi)存分配

Android性能調(diào)優(yōu)篇之探索垃圾回收機制

我們大概了解了Java內(nèi)存的一些基本知識,這個對于本篇文章的要講的內(nèi)存泄露掉冶,還是挺有幫助的。

本來最開始就想寫關(guān)于內(nèi)存泄露的文章的荐开,由于它涉及了一些Java內(nèi)存的基本知識,所以為了鋪墊简肴,寫下了內(nèi)存分配機制以及垃圾回收的兩篇文章誓焦。

關(guān)于內(nèi)存泄露,Memory Leak,我想基本上所有開發(fā)人員都多多少少接觸過這個概念着帽,因為它確實與我們的實際開發(fā)脫不了干系杂伟。這次我講述內(nèi)存泄露的角度主要是從Android實際開發(fā)的角度。


技術(shù)詳情

1.什么是內(nèi)存泄露

所謂內(nèi)存泄露仍翰,就是指我們不再使用的對象持續(xù)占有內(nèi)存赫粥,或者這些不再使用的對象沒有辦法得到及時釋放(GC Roots依然可達),而導(dǎo)致內(nèi)存空間的浪費予借。值得注意的是越平,我們App的內(nèi)存泄露的不斷積累,最終會導(dǎo)致OOM(Out Of Memory)灵迫,更嚴(yán)重的導(dǎo)致程序崩潰秦叛,所以我們平時一定要處理內(nèi)存泄露。

2.Android中的內(nèi)存泄露

2.1 單例

我們通過代碼瀑粥,來看一下單例模式產(chǎn)生的內(nèi)存泄露挣跋。

首先是單例類OyTestManager.java(這里就不寫關(guān)于實際業(yè)務(wù)的代碼了):

import android.content.Context;

/**
 * *****************************************************************
 * * 文件作者:ouyangshengduo
 * * 創(chuàng)建時間:2017/8/14
 * * 文件描述:單例模式演示內(nèi)存泄露
 * * 修改歷史:2017/8/14 21:41*************************************
 **/

public class OyTestManager {

    private static OyTestManager mInstance;
    private Context mContext;

    private OyTestManager(Context mContext){
        this.mContext = mContext;
    }

    public static OyTestManager getmInstance(Context mContext){
        if(null == mInstance){
            synchronized (OyTestManager.class){
                if(null == mInstance){
                    mInstance = new OyTestManager(mContext);
                }
            }
        }
        return mInstance;
    }
}


然后再MainActivity.java里面使用這個單例:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();

    }

    /**
     * 數(shù)據(jù)初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }
}

代碼非常簡單,這里只是為了講解我們實際開發(fā)中使用單例造成的內(nèi)存泄露狞换,通過上面的代碼避咆,我們使用Android Studio里面的Android Monitor進行內(nèi)存泄露的分析(也可以使用其他的工具,如MAT)修噪,分析步驟為:

1 將以上代碼在Android設(shè)備上跑起來查库,然后點擊返回退出軟件

2.點擊Android Studio的Android Monitor中的Initiate GC,觸發(fā)系統(tǒng)的一次GC,具體操作截圖如下:

觸發(fā)GC
觸發(fā)GC

3.然后點擊Initiate GC旁邊的Dump Java Heap,將此時的系統(tǒng)的Java堆的情況導(dǎo)出來黄琼,稍等一會樊销,會生成一個.hprof的文件
具體操作截圖如下:

導(dǎo)出堆狀態(tài)
導(dǎo)出堆狀態(tài)

4.點擊任務(wù)分析按鈕,開始分析脏款,分析結(jié)束會有一個分析結(jié)果围苫,具體查看分析結(jié)果中的內(nèi)容,看我們的軟件退出之后弛矛,是否還有資源沒有得到釋放

調(diào)出分析界面
調(diào)出分析界面
開始分析
開始分析

從以上操作中够吩,我們能夠看出,我們的MainActivity已經(jīng)退出了丈氓,但系統(tǒng)并沒有回收掉這個MainActivity,因為在MainActivity中使用了單例模式周循,mInstance這個靜態(tài)對象與MainAcitivty依然存在引用關(guān)系,從之前的內(nèi)存相關(guān)的知識可以知道万俗,mInstance在這里就可以作為一個GC Root湾笛,因為從GC Root開始進行搜索,對于MainActivity這個對象是可達的闰歪,所以嚎研,系統(tǒng)沒有回收掉這個對象。

知道了原因,我們就可以對其進行優(yōu)化了临扮。

我們知道單例的靜態(tài)特性與我們的App的生命周期是一樣長的论矾,所以,我們只需要把MainActivity的引用替換成我們的ApplictionContext,這樣杆勇,系統(tǒng)就能回收掉MainActivity對象了贪壳,以下是單例模式進行優(yōu)化后的寫法:


import android.content.Context;

/**
 * *****************************************************************
 * * 文件作者:ouyangshengduo
 * * 創(chuàng)建時間:2017/8/14
 * * 文件描述:單例模式演示內(nèi)存泄露
 * * 修改歷史:2017/8/14 21:41*************************************
 **/

public class OyTestManager {

    private static OyTestManager mInstance;
    private Context mContext;

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

    public static OyTestManager getmInstance(Context mContext){
        if(null == mInstance){
            synchronized (OyTestManager.class){
                if(null == mInstance){
                    mInstance = new OyTestManager(mContext);
                }
            }
        }
        return mInstance;
    }

}

在構(gòu)造方法中this.mContext = mContext 改成了:this.mContext = mContext.getApplicationContext();

通過以上的寫法,我們再進行上面那種分析方法蚜退,就不會出現(xiàn)Leaked Activity這一項了闰靴,也就意味著,我們已經(jīng)對這個單例優(yōu)化成功了钻注。

2.2 匿名內(nèi)被類

匿名內(nèi)部類蚂且,在Java當(dāng)中,非靜態(tài)內(nèi)部類默認將會有持有外部類的引用幅恋,當(dāng)在內(nèi)部類實例化一個靜態(tài)的對象杏死,那么,這個對象將會與App的生命周期一樣長佳遣,又因為非靜態(tài)內(nèi)部類一直持有外部的MainActivity的引用识埋,導(dǎo)致MainActivity無法被回收,內(nèi)存泄露的代碼如下:


import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();
    }

    /**
     * 數(shù)據(jù)初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }

    //定義一個內(nèi)部類
    class LeakTest{
        private static final String TAG = "Just a test";
    }
}

用上面的分析方法區(qū)分析零渐,同樣會出現(xiàn)Leaked Activities,也就是存在內(nèi)存泄露

內(nèi)部類分析
內(nèi)部類分析

這種情況窒舟,我們需要把匿名內(nèi)部類修改為靜態(tài)內(nèi)部類,靜態(tài)內(nèi)部類诵盼,這樣靜態(tài)內(nèi)部類就不會持有外部MainActivity的引用惠豺,從而不會有內(nèi)存泄露的問題,優(yōu)化后的代碼如下:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {


    private OyTestManager oyTestManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initData();
    }

    /**
     * 數(shù)據(jù)初始化
     */
    private void initData(){

        oyTestManager = OyTestManager.getmInstance(this);

    }

    //定義一個內(nèi)部類
    static class LeakTest{
        private static final String TAG = "Just a test";
    }
}

2.3 Handler

Handler 我們應(yīng)該比較熟悉风宁,我們通常使用它來進行子線程到主線程的UI更新洁墙。不過,我們實際開發(fā)中戒财,因為Handler而造成的內(nèi)存泄漏是最常見的热监,比如說,我們平時處理一些網(wǎng)絡(luò)數(shù)據(jù)獲取的時候饮寞,會請求一個回調(diào)孝扛,然后我們會使用Handler進行處理。這個時候幽崩,如果我們沒有考慮到內(nèi)存泄露苦始,就會造成比較嚴(yán)重的問題。

首先慌申,我們平時使用Handler 都是這樣的:

private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //業(yè)務(wù)邏輯代碼
        }
    };

我們來分析一下這種寫法:

1.mHandler在這里是Handler的非靜態(tài)內(nèi)部類的一個實例陌选,會持有外部類MainActiivty的引用

2.Handler的消息隊列是在Looper線程中不斷輪詢處理消息,當(dāng)我們的MainActivity退出
的時候,消息隊列中還有未處理的消息或者正在處理消息咨油,而消息隊列中的Message又持
有mHandler的實例引用您炉,而且,mHandler也持有外部類MainActivity的引用臼勉,導(dǎo)致系統(tǒng)
無法對MainActivity進行回收邻吭,而造成內(nèi)存泄露

所以,這種寫法無法保證mHandler的生命周期與MainActivity一樣宴霸,很經(jīng)常造成內(nèi)存泄露。

而正確的寫法是把Handler改成MainActivity的一個靜態(tài)內(nèi)部類膏蚓,同時在其內(nèi)部持有外部類的弱引用瓢谢,這樣就能解決好這個內(nèi)存泄露問題:

private MyHandler mHandler = new MyHandler(this);

private static class MyHandler extends Handler{
    private WeakReference<Context> reference;
    public MyHandler(Context context){
        reference = new WeakReference<Context>(context);
    }

    @Override
    public void handleMessage(Message msg) {
        MainActivity mainActivity = (MainActivity) reference.get();
        if(mainActivity != null){
            //業(yè)務(wù)處理邏輯
        }
    }
}

2.4 盡量避免使用static變量

我們平時實際開發(fā)中,經(jīng)常會使用static的變量驮瞧,能夠在不同的類和包中使用氓扛,但我們需要知道,static變量的還是有一些坑的:


1.占用內(nèi)存论笔,系統(tǒng)一般不會進行釋放

2.當(dāng)系統(tǒng)內(nèi)存不夠用的時候采郎,會自動回收靜態(tài)內(nèi)存,這樣就有可能導(dǎo)致我們的程序訪問
某一個靜態(tài)對象的時候發(fā)生不可預(yù)測的錯誤狂魔。

3.當(dāng)Android App退出的時候蒜埋,進程并沒有馬上退出,app的一些靜態(tài)變量還存在內(nèi)存中最楷,這是不安全的整份。

因此,我們使用static變量的時候的籽孙,必須考慮好真的有沒有必要使用static模型烈评,一旦static使用的不合理,會造成大量的內(nèi)存浪費犯建。很多時候讲冠,我們可以在Application
里聲明定義全局變量,或者使用持久化數(shù)據(jù)存儲來保存全局變量适瓦。

2.5 資源未關(guān)閉造成的內(nèi)存泄漏

資源未關(guān)閉的情況竿开,這個我們平時開發(fā)中,應(yīng)該會比較重視犹菇,因為特別容易出現(xiàn)內(nèi)存泄露德迹,最終導(dǎo)致程序內(nèi)存溢出而崩潰,因為容易呈現(xiàn)揭芍,所以我們知道其必要性胳搞。

當(dāng)我們使用了BroadcastReceiver,ContentObserverr,File,Cursor,Stream,Bitmap等資源的時候,使用完一定要記得及時關(guān)閉或者銷毀。

例如肌毅,我們一般在某個Activity中register了某一個廣播BroadcastReceiver,在Activity結(jié)束的時候沒有調(diào)用unregister,這明顯就會造成內(nèi)存泄露筷转。

還有的時候使用查詢數(shù)據(jù)庫,讀取文件等一些資源型對象的時候悬而,一定要記得調(diào)用關(guān)閉的方法呜舒。

還就是Bitmap的調(diào)用了,這個東西特別占用內(nèi)存笨奠,使用完可以調(diào)用Bitmap.recycle()方法回收此對象的像素所占用的內(nèi)存袭蝗。

2.6 AsnycTask造成的內(nèi)存泄露

其實AsnycTask造成的內(nèi)存泄露的原理與Handler是一樣的,主要還是因為非靜態(tài)匿名內(nèi)部類持有外部類的引用般婆,在AsnycTask的doInBackground的方法中到腥,可能還有任務(wù)正在處理,從而導(dǎo)致外部類Activity不能被釋放蔚袍。

解決方案也可以使用靜態(tài)內(nèi)部類乡范,也可以在Activity的onDestory方法中調(diào)用cancle方法,我這里就不貼代碼了啤咽。


干貨總結(jié)

以上介紹了我們Android開發(fā)中晋辆,經(jīng)常遇到的六種內(nèi)存泄露的情況,實際上內(nèi)存泄露遠不及這六種宇整,牽扯到方方面面瓶佳,很多時候,有些內(nèi)存泄露并不能百分百的去解決没陡,需考慮一些系統(tǒng)的權(quán)衡來制定方案涩哟。關(guān)于內(nèi)存泄露,有些點在我們編碼過程中還是需要再次重申一下:

1.恰當(dāng)使用單例模式盼玄,Handler機制

2.資源對象使用完了贴彼,一定要記得關(guān)閉或者釋放掉

3.老生常談的ListView,一定要記得使用緩存convertView

4.Bitmap對象使用完了要記得調(diào)用recycle()方法來釋放底層C那一塊的內(nèi)存

5.可能的話,將Activity的相關(guān)的context,用Application的context來替代

6.集合當(dāng)中存放的對象引用埃儿,某個對象使用完了器仗,記得從這個集合當(dāng)中清理掉該引用

7.匿名內(nèi)部類也要慎用,可能的話童番,用靜態(tài)內(nèi)部類替代精钮,避免持有外部類引用而導(dǎo)致外部類無法被回收

8.熟悉內(nèi)存泄露檢查和分析的工具,如MAT,LeakCanary,Android Monitor等工具
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剃斧,一起剝皮案震驚了整個濱河市轨香,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌幼东,老刑警劉巖臂容,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件科雳,死亡現(xiàn)場離奇詭異,居然都是意外死亡脓杉,警方通過查閱死者的電腦和手機糟秘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來球散,“玉大人尿赚,你說我怎么就攤上這事〗堆撸” “怎么了凌净?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嘁灯。 經(jīng)常有香客問我泻蚊,道長,這世上最難降的妖魔是什么丑婿? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮没卸,結(jié)果婚禮上羹奉,老公的妹妹穿的比我還像新娘。我一直安慰自己约计,他們只是感情好诀拭,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著煤蚌,像睡著了一般耕挨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尉桩,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天筒占,我揣著相機與錄音,去河邊找鬼蜘犁。 笑死翰苫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的这橙。 我是一名探鬼主播奏窑,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼屈扎!你這毒婦竟也來了埃唯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤鹰晨,失蹤者是張志新(化名)和其女友劉穎墨叛,沒想到半個月后止毕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡巍实,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年滓技,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棚潦。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡令漂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丸边,到底是詐尸還是另有隱情叠必,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布妹窖,位于F島的核電站纬朝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏骄呼。R本人自食惡果不足惜共苛,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜓萄。 院中可真熱鬧隅茎,春花似錦、人聲如沸嫉沽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绸硕。三九已至堂竟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玻佩,已是汗流浹背出嘹。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留夺蛇,地道東北人疚漆。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像刁赦,于是被迫代替她去往敵國和親娶聘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

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