Android內(nèi)存泄漏那些事兒

最近在項(xiàng)目中偶爾會(huì)發(fā)現(xiàn)內(nèi)存泄漏現(xiàn)象。一開始還是一臉懵逼的查來(lái)查去郁稍,一直沒(méi)有個(gè)清晰地思路。這幾天閑下來(lái)胜宇,打算認(rèn)真整理學(xué)習(xí)一下艺晴。我在這里從一個(gè)“如何主動(dòng)造成內(nèi)存泄漏”的角度來(lái)學(xué)習(xí),然后熟悉一下不同方法檢測(cè)的結(jié)果如何掸屡,這樣以后再遇到相關(guān)問(wèn)題時(shí)就能夠很快的解決了封寞。

java gc

首先要有一個(gè)大前提,也就是java gc仅财。在大部分虛擬機(jī)(包括Android的ART)中狈究,Java都采用了“可達(dá)性分析”算法來(lái)進(jìn)行內(nèi)存回收,原理是:會(huì)有幾個(gè)引用作為root節(jié)點(diǎn)盏求,對(duì)于任意對(duì)象來(lái)說(shuō)抖锥,如果從root層層遍歷,如果找不到對(duì)于他的引用鏈碎罚,那么這個(gè)對(duì)象就被標(biāo)記為無(wú)用磅废,就會(huì)在gc時(shí)被銷毀。

何為泄漏

內(nèi)存泄漏荆烈,即部分對(duì)象雖然已經(jīng)不再使用拯勉,但是因?yàn)橛衦oot持有引用,所以并沒(méi)有被銷毀憔购,所占用的內(nèi)存一直沒(méi)有被釋放宫峦。一次兩次發(fā)生影響不大。如果頻繁發(fā)生玫鸟,那么可用內(nèi)存會(huì)漸漸不足导绷,最終在某一次請(qǐng)求內(nèi)存時(shí)發(fā)現(xiàn)內(nèi)存不足而發(fā)生oom。這里要明確一個(gè)概念屎飘,只有強(qiáng)引用會(huì)發(fā)生內(nèi)存泄漏妥曲,而weak等引用因?yàn)槠涮厥鈾C(jī)制贾费,所以影響不大。

什么會(huì)泄露

泄露影響比較大的就是一些大對(duì)象檐盟,常見(jiàn)的比如某些資源褂萧,bitmap,以及activity遵堵。

如何發(fā)生泄露

首先讓我們從另一個(gè)角度來(lái)看箱玷,如何主動(dòng)發(fā)生內(nèi)存泄漏呢怨规?當(dāng)然是想辦法給他一個(gè)一直存在的強(qiáng)引用了陌宿。

static

static這個(gè)關(guān)鍵字使一個(gè)變量變?yōu)橹缓瓦@個(gè)類相關(guān)的類變量,和實(shí)例無(wú)關(guān)波丰。他的生命周期是很長(zhǎng)的壳坪,貫穿于app的啟動(dòng)到關(guān)閉。因此只要用一個(gè)static引用一個(gè)大對(duì)象掰烟,就可以泄漏了爽蝴!舉個(gè)例子:

static Activity activity;

這是最簡(jiǎn)單粗暴的持有一個(gè)activity的引用,這樣這個(gè)activity退出之后對(duì)象并沒(méi)有被銷毀纫骑。

static View view;

一個(gè)View初始化時(shí)會(huì)用到context蝎亚,我們?cè)谧远xView,重寫構(gòu)造方法時(shí)就知道這個(gè)了先馆。因此如果一個(gè)View也像這樣被持有发框,那個(gè)context也不會(huì)被釋放。

innerClass

內(nèi)部類有個(gè)特性煤墙,是他會(huì)持有一個(gè)外部類的引用梅惯。如果內(nèi)部類的實(shí)例一直存活,那么外部類activity的實(shí)例也就一直在仿野。比如持有一個(gè)static的內(nèi)部類引用:

static LeakInnerClass context;

class LeakInnerClass {
    Context context;
}

或者以前我們用asynctask時(shí)喜歡搞一個(gè)匿名內(nèi)部類執(zhí)行異步任務(wù)铣减,那當(dāng)我們activity退出后這個(gè)異步任務(wù)還在執(zhí)行的話,就會(huì)泄露了脚作。

void leakAsyncTask(){
    new AsyncTask<Void,Void,Void>(){

        @Override
        protected Void doInBackground(Void... params) {
            while(true){
              //哇啦啦啦啦啦啦我就是耗時(shí)操作
            }
            return null;
        }
    };
}

還有自己開個(gè)匿名線程:

void leakThread(){
    new Thread(){
        @Override
        public void run() {
            while (true){
                //哇啦啦啦啦啦啦我是耗時(shí)操作
            }
        }
    }.start();
}

還有在使用handler時(shí)葫哗,如果用了匿名handler,那么這個(gè)handler會(huì)帶著activity的引用藏到消息隊(duì)列中球涛。消息沒(méi)有被處理魄梯,就會(huì)造成內(nèi)存泄漏。類似的宾符,還有timertask等酿秸。

register

我們平時(shí)會(huì)用到很多第三方庫(kù),比如ButterKnife EventBus RxJava等等魏烫,有的時(shí)候要獲取系統(tǒng)服務(wù)辣苏,getSystemService肝箱。在使用的時(shí)候,都有一個(gè)先registerd或者bind的操作稀蟋,而且在創(chuàng)建的時(shí)候會(huì)把a(bǔ)ctivity的引用傳過(guò)去煌张。如果在activity結(jié)束時(shí)沒(méi)有unregister或者unbind,就會(huì)造成內(nèi)存泄漏退客。

如何檢測(cè)泄漏

最簡(jiǎn)單的方法自然就是使用leakcanary了骏融。只要給自己的項(xiàng)目加上這個(gè)工具,在發(fā)生泄漏的時(shí)候很快就會(huì)有提示萌狂。具體使用方法看這里档玻。

除此之外,android studio的刀耕火種的方式也不錯(cuò)茫藏,在這里我拿一個(gè)例子來(lái)示范一下我是怎么用的误趴。

一次leak檢測(cè)過(guò)程

準(zhǔn)備工作

首先,我寫了兩個(gè)activity务傲,一個(gè)MainActivity凉当,一個(gè)MemoryLeakActivity,邏輯是:MainActivity中有個(gè)按鈕售葡,點(diǎn)擊會(huì)調(diào)到MemoryLeakActivity看杭,在這個(gè)activity中會(huì)故意發(fā)生內(nèi)存泄漏,代碼如下:

image

在開始之前挟伙,再熟悉一下這個(gè)

image

(原諒我拙劣的畫筆)

這個(gè)Monitors可以觀察當(dāng)前選中app的運(yùn)行狀態(tài)楼雹,現(xiàn)在只需要關(guān)注我標(biāo)了123的地方。

首先這個(gè)Memory就是當(dāng)前app的內(nèi)存使用狀況:

1.產(chǎn)生一個(gè)當(dāng)前java堆的.hprof文件像寒,這個(gè)文件反映了當(dāng)前時(shí)刻java堆中內(nèi)存詳情烘豹,記住這個(gè)玩意有大用!

2.手動(dòng)進(jìn)行一次gc

3.這一塊很重要诺祸,首先他有兩個(gè)部分携悯,藍(lán)色和灰色。藍(lán)色部分是當(dāng)前內(nèi)存使用大小筷笨,灰色部分是這個(gè)app被限制的最大內(nèi)存大小憔鬼。當(dāng)藍(lán)色部分越來(lái)越大,最后和灰色部分一樣時(shí)胃夏,說(shuō)明我們內(nèi)存使用很多了即將內(nèi)存不足轴或,此時(shí)會(huì)進(jìn)行一次gc同時(shí)將回灰色部分即限制的大小提高。

肉眼觀察

好了仰禀,介紹完這個(gè)工具照雁,我們開始動(dòng)手實(shí)踐。首先打開app答恶,點(diǎn)擊按鈕跳到會(huì)發(fā)生泄漏的activity上饺蚊,再按返回鍵萍诱,然后再次按下按鈕……這樣反復(fù)操作:

image
image

與此同時(shí),觀察monitors的memory窗口污呼,會(huì)發(fā)現(xiàn)藍(lán)色部分在每一次開啟新activity時(shí)會(huì)增長(zhǎng)一部分裕坊,這很正常。但是在返回時(shí)燕酷,明明activity被“退出”了籍凝,但是藍(lán)色部分還是沒(méi)有變化。反復(fù)幾次之后苗缩,藍(lán)色部分一直在增長(zhǎng)饵蒂。也就是說(shuō)當(dāng)前內(nèi)存越用越多,可以推斷已經(jīng)發(fā)生內(nèi)存泄漏啦~

自動(dòng)分析

接下來(lái)由android studio來(lái)分析一下挤渐。在反復(fù)幾次上面的操作之后苹享,返回MainActivity双絮,然后點(diǎn)擊dump java heap按鈕浴麻,然后等一會(huì)兒,android studio在為我們dump此時(shí)的horof文件囤攀。在成功后软免,會(huì)自動(dòng)打開:

image

如圖在這個(gè)界面中,我們看最右面有一個(gè)欄叫 Analyzer Tasks焚挠,打開它膏萧,會(huì)發(fā)現(xiàn)有兩個(gè)選項(xiàng)。我們是來(lái)看activity的內(nèi)存泄漏的蝌衔,那就把那個(gè)查重復(fù)字符串的√去掉榛泛。然后點(diǎn)右邊那個(gè)綠色小三角,會(huì)發(fā)現(xiàn)下面Analysis Results欄里面展示出了當(dāng)前泄露的Activity引用:

image

點(diǎn)擊第一個(gè)item噩斟,最下方Reference Tree欄中便展示出了具體的引用:

image

一般來(lái)說(shuō)曹锨,第一個(gè)就是我們發(fā)生泄漏的地方。在圖中剃允,this$0的意思是隱式的引用沛简。也就是說(shuō),我們的activity是因?yàn)橐粋€(gè)內(nèi)部類而發(fā)生了內(nèi)存泄漏斥废。

再點(diǎn)擊剛才results中第二個(gè)item椒楣,看一下下方的reference tree:

image

可以看到顯式的有一個(gè)leakCntextRef引用,這說(shuō)明我們有一個(gè)名為leakCntextRef的引用持有了activity牡肉∨趸遥回過(guò)頭看看我們的代碼,果然统锤,驗(yàn)證的沒(méi)錯(cuò)毛俏。

拓展

android studio的分析還算比較簡(jiǎn)單而且內(nèi)容較少吩屹,我們可以把這個(gè)hprof導(dǎo)出,然后用mat來(lái)分析拧抖,具體看這里煤搜。

怎么解決泄漏

既然發(fā)生了泄漏,那就要解決它唧席,避免問(wèn)題出現(xiàn)擦盾。那么怎么解決呢?很簡(jiǎn)單淌哟,泄漏是因?yàn)槌钟辛薬ctivity引用導(dǎo)致無(wú)法被銷毀迹卢,那么只有兩個(gè)選擇:及時(shí)取消引用,或者讓這個(gè)引用多待一會(huì)徒仓,但是該gc的時(shí)候就銷毀腐碱。

根據(jù)這個(gè)思路:

  • 我們?cè)诖a中能不用static變量持有contxt就不用,非要用就用weak引用掉弛。
  • 對(duì)于內(nèi)部類症见,盡量用靜態(tài)內(nèi)部類,這樣就不會(huì)持有外部類引用殃饿。如果需要外部類引用做一些事谋作,就手動(dòng)賦給一個(gè)weak引用。
  • 對(duì)于匿名內(nèi)部類乎芳,不要圖簡(jiǎn)單方便遵蚜,實(shí)在不行就乖乖的寫成外部類。
  • 異步操作奈惑,盡量用可以方便管理的吭净,比如rxJava,而不是用老古董AsyncTask了肴甸。非要用也最好加一個(gè)終止條件寂殉,在退出Activity時(shí)就該結(jié)束了。
  • 在用rx時(shí)雷滋,可以在subscribe()的時(shí)候獲取到Subscripeion不撑,在不用的時(shí)候手動(dòng)unSubscribe(),或者直接bind()到Activity的生命周期上晤斩,比如使用RxActivity管理焕檬。
  • 在使用handler時(shí),記得在activity的onDestroy()中加上remove()
  • 在獲取到某些資源時(shí)澳泵,使用完記得釋放
  • 在用到一些大對(duì)象比如Bitmap啊什么的实愚,要記得回收
  • 最后,在使用各種第三方庫(kù)或者系統(tǒng)服務(wù)的時(shí)候還要記得有注冊(cè)或綁定就要有解除注冊(cè)、解綁定腊敲。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末击喂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子碰辅,更是在濱河造成了極大的恐慌懂昂,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件没宾,死亡現(xiàn)場(chǎng)離奇詭異凌彬,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)循衰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門铲敛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人会钝,你說(shuō)我怎么就攤上這事伐蒋。” “怎么了迁酸?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵先鱼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我胁出,道長(zhǎng)型型,這世上最難降的妖魔是什么段审? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任全蝶,我火速辦了婚禮,結(jié)果婚禮上寺枉,老公的妹妹穿的比我還像新娘抑淫。我一直安慰自己,他們只是感情好姥闪,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布始苇。 她就那樣靜靜地躺著,像睡著了一般筐喳。 火紅的嫁衣襯著肌膚如雪催式。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天避归,我揣著相機(jī)與錄音荣月,去河邊找鬼。 笑死梳毙,一個(gè)胖子當(dāng)著我的面吹牛哺窄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼萌业,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坷襟!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起生年,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤婴程,失蹤者是張志新(化名)和其女友劉穎膀曾,沒(méi)想到半個(gè)月后晚胡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡连舍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年授段,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蹲蒲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡侵贵,死狀恐怖届搁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窍育,我是刑警寧澤卡睦,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站漱抓,受9級(jí)特大地震影響表锻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜乞娄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一瞬逊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仪或,春花似錦确镊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至到旦,卻和暖如春旨巷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背添忘。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工采呐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昔汉。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓懈万,卻偏偏與公主長(zhǎng)得像拴清,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子会通,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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