關(guān)于Android 內(nèi)存泄漏的分享

前情提要

java中四種引用類型

StrongReference強(qiáng)引用

如 Object o = new Object()

  • 回收時(shí)機(jī):從不回收
  • 使用:對(duì)象的一般保存
  • 生命周期:JVM停止的時(shí)候才會(huì)終止
SoftReference軟引用
  • 回收時(shí)機(jī):當(dāng)內(nèi)存不足的時(shí)候咆繁;
  • 使用:SoftReference結(jié)合- ReferenceQueue構(gòu)造有效期短;
  • 生命周期:內(nèi)存不足時(shí)終止
WeakReference,弱引用
  • 回收時(shí)機(jī):在垃圾回收的時(shí)候眼坏;
  • 使用:同軟引用;
  • 生命周期:GC后終止
PhatomReference 虛引用
  • 回收時(shí)機(jī):在垃圾回收的時(shí)候;
  • 使用:合ReferenceQueue來(lái)跟蹤對(duì)象唄垃圾回收期回收的活動(dòng);
  • 生命周期:GC后終止

Java 程序運(yùn)行時(shí)的內(nèi)存分配

Java 程序運(yùn)行時(shí)的內(nèi)存分配策略有三種:靜態(tài)分配、棧式分配和堆式分配服猪。
對(duì)應(yīng)的存儲(chǔ)區(qū)域如下:

  • 靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量拐云。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好罢猪,并且在程序整個(gè)運(yùn)行期間都存在。

  • 棧區(qū) :方法體內(nèi)的局部變量都在棧上創(chuàng)建叉瘩,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放膳帕。

  • 堆區(qū) : 又稱動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接 new 出來(lái)的內(nèi)存。這部分內(nèi)存在不使用時(shí)將會(huì)由 Java 垃圾回收器來(lái)負(fù)責(zé)回收危彩。

棧和堆的區(qū)別

棧內(nèi)存:在方法體內(nèi)定義的局部變量(一些基本類型的變量和對(duì)象的引用變量)都是在方法的棧內(nèi)存中分配的攒磨。當(dāng)在一段方法塊中定義一個(gè)變量時(shí),Java 就會(huì)在棧中為該變量分配內(nèi)存空間汤徽,當(dāng)超過(guò)該變量的作用域后娩缰,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用谒府。

堆內(nèi)存:用來(lái)存放所有由 new 創(chuàng)建的對(duì)象(包括該對(duì)象其中的所有成員變量)和數(shù)組拼坎。在堆中分配的內(nèi)存泰鸡,將由 Java 垃圾回收器來(lái)自動(dòng)管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量欧芽,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址库正,這個(gè)特殊的變量就是我們上面說(shuō)的引用變量。我們可以通過(guò)這個(gè)引用變量來(lái)訪問(wèn)堆中的對(duì)象或者數(shù)組喷楣。

棧內(nèi)存:基本類型變量、對(duì)象引用變量 、方法內(nèi)局部變量
堆內(nèi)存: new 出來(lái)的對(duì)象

看下面代碼

public class A {

    int a = 0; // 棧內(nèi)

    B b = new B(); // new B()堆內(nèi)  b在棧內(nèi)
    public void test(){
        int a1 = 1;  //棧內(nèi)
        B b1 = new B(); // b1在棧內(nèi) new B() 在堆內(nèi)
    }
}

A object = new A(); //object棧內(nèi)  new A() 堆內(nèi)

A類內(nèi)的局部變量都存在于棧中岛蚤,包括基本數(shù)據(jù)類型a1和引用變量b1,b1指向的B對(duì)象實(shí)體存在于堆中

引用變量object存在于棧中,而object指向的對(duì)象實(shí)體存在于堆中。new A 對(duì)象的所有成員變量a和b在棧內(nèi)(句柄),而引用變量b指向的B類對(duì)象實(shí)體存在于堆中。

主線程的Looper對(duì)象的生命周期 = 該應(yīng)用程序的生命周期
在Java中,非靜態(tài)內(nèi)部類 & 匿名內(nèi)部類都默認(rèn)持有 外部類的引用

舉例handler內(nèi)部msg —— handler實(shí)例 ——Activity實(shí)例


造成內(nèi)存泄漏情景

  • 非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄露群叶,比如Handler埠通,解決方法是將內(nèi)部類寫(xiě)成靜態(tài)內(nèi)部類端辱,在靜態(tài)內(nèi)部類中使用軟引用/弱引用持有外部類的實(shí)例

  • IO操作后,沒(méi)有關(guān)閉文件導(dǎo)致的內(nèi)存泄露,比如Cursor鸡岗、FileInputStream、FileOutputStream使用完后沒(méi)有關(guān)閉

  • 自定義View中使用TypedArray后,沒(méi)有recycle

  • Context 造成的內(nèi)存泄漏 如單例模式中的內(nèi)存泄漏卸察。解決方法:使用Application的Context

  • 注冊(cè)監(jiān)聽(tīng)器的泄漏 沒(méi)有在destory時(shí) unregisterxxx()

  • 集合中對(duì)象沒(méi)清理造成的內(nèi)存泄漏 解決方法 :在Activity退出之前涡扼,將集合里的東西clear什猖,然后置為null,再退出程序

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


adb dumpsys meminfo packageName 查找內(nèi)存泄漏

對(duì)比兩次的Activity和View的數(shù)量變化.png

adb shell dumpsys meminfo packagename -d命令芯咧,反復(fù)進(jìn)入、退出同一界面,并對(duì)比兩次的Activity和View的數(shù)量變化敬飒。如果有差異邪铲,則說(shuō)明存在內(nèi)存泄露(在使用命令查看Activity和View的數(shù)量之前,記得手動(dòng)觸發(fā)GC)无拗。

native leak.png

要繼續(xù)觀察dumpsys meminfo 包名带到, 輸出的結(jié)果信息,關(guān)注點(diǎn)放在 UnKnown那一行 和 Native Heap 那一行英染,關(guān)注Heap Alloc 或者 Pss Total, 如果你的總TOTAL一直再增加揽惹,但是是由于這兩行的增加,那么這個(gè)問(wèn)題你不需要再繼續(xù)在MAT上花時(shí)間了四康,因?yàn)檫@種內(nèi)存泄露問(wèn)題搪搏,出在Native層(C)那么你需要去找你程序中使用到JNI的地方,so庫(kù)或者其他一些特殊調(diào)用上闪金,分析它們是否可能造成內(nèi)存泄露問(wèn)題疯溺。

adb shell showmap -a PID

然興許你依舊沒(méi)有頭緒,那么沒(méi)關(guān)系毕泌,另一個(gè)命令就是為了你而存在的喝检,(首先某個(gè)應(yīng)用的PID號(hào), 用dumpsys meminfo 包名,那邊已經(jīng)可以查到)

譬如我上面那個(gè)mms, PID號(hào)為2786撼泛, 接著adb shell showmap -a PID號(hào) (adb shell showmap -a 2786)

然后根據(jù)結(jié)果[....]這的信息挠说,在去google上面找關(guān)鍵字, 譬如:[ anon ] bash的堆

(4)當(dāng)你最終還是不知道是由哪邊的.so庫(kù)引起的話愿题,你可以查看下Native Heap的內(nèi)存分配情況损俭,這時(shí)候你依舊需要借助DDMS,

需要先執(zhí)行以下命令:

adb shell setprop libc.debug.malloc 1

adb shell stop

adb shell start

然后你還需要改一下eclipse中的配置參數(shù)值【因?yàn)槿绻悴慌渲玫脑捙诵铮愕腄DMS打開(kāi)默認(rèn)是看不到Native Heap那個(gè)Tab項(xiàng)的】

在ddms.cfg文件(實(shí)在找不到的話杆兵,就用Everything搜索下吧)最后增加一行native=true并save。ddms.cfg位于c:\Users\xxx.android目錄下仔夺。

在Device中選擇好你要的應(yīng)用的包名項(xiàng)琐脏,然后按下Snapshot按鈕, 就可以觀察到Native Heap的使用情況了缸兔,然后反復(fù)執(zhí)行腳本日裙,再觀察觀察,你會(huì)找到你需要的東西的惰蜜。


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

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
        //this.context = context.getApplicationContext(); 解決方式
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

2.Handler造成的內(nèi)存泄漏

當(dāng) Android 應(yīng)用程序啟動(dòng)時(shí)昂拂,framework 會(huì)為該應(yīng)用程序的主線程創(chuàng)建一個(gè) Looper 對(duì)象。Looper 對(duì)象包含一個(gè)簡(jiǎn)單的消息隊(duì)列 Message Queue抛猖,并且能夠循環(huán)的處理隊(duì)列中的消息格侯。這些消息包括大多數(shù)應(yīng)用程序 framework 事件鼻听,例如 Activity 生命周期方法調(diào)用、button 點(diǎn)擊等联四,這些消息都會(huì)被添加到消息隊(duì)列中并被逐個(gè)處理撑碴。主線程的 Looper 對(duì)象會(huì)伴隨該應(yīng)用程序的整個(gè)生命周期。

當(dāng)我們?cè)谥骶€程中實(shí)例化一個(gè) Handler 對(duì)象后碎连,會(huì)自動(dòng)與主線程 Looper 的消息隊(duì)列關(guān)聯(lián)起來(lái)灰羽。所有發(fā)送到消息隊(duì)列的消息 Message 都會(huì)擁有一個(gè)對(duì) Handler 的引用,而此時(shí)當(dāng)前 Activity 如果已經(jīng)結(jié)束/銷毀鱼辙,而 Handler 由于是非靜態(tài)內(nèi)部類就會(huì)持有外部類的對(duì)象廉嚼,抓住當(dāng)前 Activity 對(duì)象不放,此時(shí)就極有可能導(dǎo)致內(nèi)存泄漏倒戏。

public class SampleActivity extends AppCompatActivity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        },  1000 * 60 * 1);
        // Go back to the previous Activity.
        finish();
    }
}

靜態(tài)內(nèi)部類不會(huì)持有外部類的引用怠噪,其跟外部類的關(guān)系,可以看成平級(jí)杜跷。

解決辦法就是使用靜態(tài)內(nèi)部類加 WeakRefrence傍念,如下所示:

private static class MyHandler extends Handler {
        private final WeakReference<Sample2Activity> mActivity;

        public MyHandler(Sample2Activity activity) {
            mActivity = new WeakReference<Sample2Activity>(activity);
        }

        @Override
        public void handleMessage   (Message msg) {
            Sample2Activity activity = mActivity.get();
            if (activity != null) {
                // ...
            }
        }
    }

或者也可以在Activity的onDestory()中 removeCallbackandMessag(null)

3.非靜態(tài)內(nèi)部類持有外部類的實(shí)例

public class Sample4Activity extends AppCompatActivity {
    private static LeakSample mLeakSample = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mLeakSample == null){
            mLeakSample = new LeakSample();
        }
        //...
    }
    class LeakSample {
        //...
    }
}

上述代碼在 Activity 內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng) Activity 時(shí)都會(huì)使用該單例的數(shù)據(jù)(避免了資源的重復(fù)創(chuàng)建),這種寫(xiě)法卻會(huì)造成內(nèi)存泄漏葛闷,同樣因?yàn)榉庆o態(tài)內(nèi)部類持有外部類對(duì)象的原因憋槐。正確的做法為: 將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來(lái)封裝成一個(gè)單例,如果需要使用Context淑趾,請(qǐng)使用ApplicationContext阳仔。

屬性動(dòng)畫(huà)導(dǎo)致內(nèi)存泄漏
屬性動(dòng)畫(huà)中有一類無(wú)線循環(huán)的動(dòng)畫(huà),如果在當(dāng)前 Activity 中播放此類動(dòng)畫(huà)扣泊,并且沒(méi)有在結(jié)束的時(shí)候(onDestory)去停止該動(dòng)畫(huà)近范,那么動(dòng)畫(huà)會(huì)一直播放下去,盡管在界面上無(wú)法看見(jiàn)動(dòng)畫(huà)的運(yùn)轉(zhuǎn)延蟹,但是在此時(shí) Activity 的 View 會(huì)被動(dòng)畫(huà)所持有评矩,而 View 又持有當(dāng)前 Activity,最終導(dǎo)致 Activity 無(wú)法被釋放阱飘。動(dòng)畫(huà)的特征代碼如下:

animator.setRepeatCount(ValueAnimator.INFINITE);
解決辦法自然很簡(jiǎn)單斥杜,在 OnDestory() 中去取消動(dòng)畫(huà)即可。

Dialog 導(dǎo)致的內(nèi)存泄漏
在當(dāng)前 Dialog 所依附的 Activity 銷毀之前,我們沒(méi)有去將當(dāng)前的 Dialgo 銷毀(dismiss) 話也是很容易導(dǎo)致內(nèi)存泄漏的沥匈。

匿名內(nèi)部類
android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View果录,此時(shí)如果你使用了匿名類,并被異步線程持有了咐熙,那要小心了,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露

public class MainActivity extends Activity {
 ...
 Runnable ref1 = new MyRunable();
 Runnable ref2 = new Runnable() {
     @Override
     public void run() {

     }
 };
    ...
}

ref1和ref2的區(qū)別是辨萍,ref2使用了匿名內(nèi)部類棋恼。我們來(lái)看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:


image

使用 Memory Profiler 查看 Java 堆和內(nèi)存分配
深入理解 Android 之內(nèi)存泄漏
Android 內(nèi)存泄漏總結(jié)
Android 內(nèi)存泄漏分析心得

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末返弹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子爪飘,更是在濱河造成了極大的恐慌义起,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件师崎,死亡現(xiàn)場(chǎng)離奇詭異默终,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)犁罩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門齐蔽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人床估,你說(shuō)我怎么就攤上這事含滴。” “怎么了丐巫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵谈况,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我递胧,道長(zhǎng)碑韵,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任缎脾,我火速辦了婚禮祝闻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赊锚。我一直安慰自己治筒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布舷蒲。 她就那樣靜靜地躺著耸袜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牲平。 梳的紋絲不亂的頭發(fā)上堤框,一...
    開(kāi)封第一講書(shū)人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音纵柿,去河邊找鬼蜈抓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛昂儒,可吹牛的內(nèi)容都是我干的沟使。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼渊跋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼腊嗡!你這毒婦竟也來(lái)了着倾?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤燕少,失蹤者是張志新(化名)和其女友劉穎卡者,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體客们,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崇决,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了底挫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恒傻。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凄敢,靈堂內(nèi)的尸體忽然破棺而出碌冶,到底是詐尸還是另有隱情,我是刑警寧澤涝缝,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布扑庞,位于F島的核電站,受9級(jí)特大地震影響拒逮,放射性物質(zhì)發(fā)生泄漏罐氨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一滩援、第九天 我趴在偏房一處隱蔽的房頂上張望栅隐。 院中可真熱鬧,春花似錦玩徊、人聲如沸租悄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泣棋。三九已至,卻和暖如春畔塔,著一層夾襖步出監(jiān)牢的瞬間潭辈,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工澈吨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留把敢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓谅辣,卻偏偏與公主長(zhǎng)得像修赞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桑阶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

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

  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題榔组。內(nèi)存泄漏...
    _痞子閱讀 1,639評(píng)論 0 8
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題熙尉。內(nèi)存泄漏...
    apkcore閱讀 1,222評(píng)論 2 7
  • 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題。內(nèi)存泄漏大家都不陌生了搓扯,簡(jiǎn)單粗俗的講,...
    DreamFish閱讀 793評(píng)論 0 5
  • 內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問(wèn)題包归。內(nèi)存泄漏大家都不陌生了锨推,簡(jiǎn)單粗俗的講,...
    宇宙只有巴掌大閱讀 2,364評(píng)論 0 12
  • OdooRPC是一個(gè)Python包公壤,提供了一種通過(guò)RPC訪問(wèn)Odoo服務(wù)的簡(jiǎn)便方法换可。 主要功能: 1. 使用類似于...
    千年碼妖閱讀 4,826評(píng)論 0 2