一文搞懂為啥你的應(yīng)用內(nèi)存泄漏?
來(lái)源:原創(chuàng) 時(shí)間:2017-10-20 瀏覽:0 次前語(yǔ)
最近在項(xiàng)目中偶然會(huì)發(fā)現(xiàn)內(nèi)存走漏現(xiàn)象。一開(kāi)端仍是一臉懵逼的查來(lái)查去:這怎樣就走漏了?這樣居然沒(méi)走漏?一向沒(méi)有個(gè)明晰地思路。這幾天閑下來(lái),計(jì)劃仔細(xì)收拾學(xué)習(xí)一下。我在這兒從一個(gè)“怎樣主動(dòng)形成內(nèi)存走漏”的視點(diǎn)來(lái)學(xué)習(xí),然后了解一下不同辦法檢測(cè)的成果怎樣,這樣今后再遇到相關(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),關(guān)于恣意目標(biāo)來(lái)說(shuō),如果從root層層遍歷,如果找不到關(guān)于他的引證鏈,那么這個(gè)目標(biāo)就被標(biāo)記為無(wú)用,就會(huì)在gc時(shí)被毀掉。
為何走漏?

內(nèi)存走漏,即部分目標(biāo)盡管現(xiàn)已不再運(yùn)用,可是由于有root持有引證,所以并沒(méi)有被毀掉,所占用的內(nèi)存一向沒(méi)有被開(kāi)釋。一次兩次發(fā)作影響不大。如果頻頻發(fā)作,那么可用內(nèi)存會(huì)逐漸缺乏,終究在某一次懇求內(nèi)存時(shí)發(fā)現(xiàn)內(nèi)存缺乏而發(fā)作oom。這兒要清晰一個(gè)概念,只要強(qiáng)引證會(huì)發(fā)作內(nèi)存走漏,而weak等引證由于其特別機(jī)制,所以影響不大。

走漏影響比較大的就是一些大目標(biāo),常見(jiàn)的比方某些資源,bitmap,以及activity。
怎樣發(fā)作走漏
首要讓我們從另一個(gè)視點(diǎn)來(lái)看,怎樣主動(dòng)發(fā)作內(nèi)存走漏呢?當(dāng)然是想辦法給他一個(gè)一向存在的強(qiáng)引證了。
static
static這個(gè)關(guān)鍵字使一個(gè)變量變?yōu)橹缓瓦@個(gè)類(lèi)相關(guān)的類(lèi)變量,和實(shí)例無(wú)關(guān)。他的生命周期是很長(zhǎng)的,貫穿于app的啟動(dòng)到封閉。因而只要用一個(gè)static引證一個(gè)大目標(biāo),就能夠走漏了!舉個(gè)比方:
static Activity activity;
這是最簡(jiǎn)略粗獷的持有一個(gè)activity的引證,這樣這個(gè)activity退出之后目標(biāo)并沒(méi)有被毀掉。
static View view;
一個(gè)View初始化時(shí)會(huì)用到context,我們?cè)谧远xView,重寫(xiě)結(jié)構(gòu)辦法時(shí)就知道這個(gè)了。因而如果一個(gè)View也像這樣被持有,那個(gè)context也不會(huì)被開(kāi)釋。
innerClass
內(nèi)部類(lèi)有個(gè)特性,是他會(huì)持有一個(gè)外部類(lèi)的引證。如果內(nèi)部類(lèi)的實(shí)例一向存活,那么外部類(lèi)activity的實(shí)例也就一向在。比方持有一個(gè)static的內(nèi)部類(lèi)引證:
或許曾經(jīng)我們用asynctask時(shí)喜愛(ài)搞一個(gè)匿名內(nèi)部類(lèi)履行異步使命,那當(dāng)我們activity退出后這個(gè)異步使命還在履行的話(huà),就會(huì)走漏了。
還有自己開(kāi)個(gè)匿名線(xiàn)程:
還有在運(yùn)用handler時(shí),如果用了匿名handler,那么這個(gè)handler會(huì)帶著activity的引證藏到音訊行列中。音訊沒(méi)有被處理,就會(huì)形成內(nèi)存走漏。相似的,還有timertask等。
register
我們平常會(huì)用到許多第三方庫(kù),比方ButterKnife EventBus RxJava等等,有的時(shí)分要獲取體系效勞,getSystemService。在運(yùn)用的時(shí)分,都有一個(gè)先registerd或許bind的操作,并且在創(chuàng)立的時(shí)分會(huì)把a(bǔ)ctivity的引證傳過(guò)去。如果在activity完畢時(shí)沒(méi)有unregister或許unbind,就會(huì)形成內(nèi)存走漏。

怎樣檢測(cè)走漏
最簡(jiǎn)略的辦法天然就是運(yùn)用leakcanary了。只要給自己的項(xiàng)目加上這個(gè)東西,在發(fā)作走漏的時(shí)分很快就會(huì)有提示。
除此之外,android studio的刀耕火種的方法也不錯(cuò),在這兒我拿一個(gè)比方來(lái)演示一下我是怎樣用的。
準(zhǔn)備工作
首要,我寫(xiě)了兩個(gè)activity,一個(gè)MainActivity,一個(gè)MemoryLeakActivity,邏輯是:MainActivity中有個(gè)按鈕,點(diǎn)擊會(huì)調(diào)到MemoryLeakActivity,在這個(gè)activity中會(huì)成心發(fā)作內(nèi)存走漏,代碼如下:
在開(kāi)端之前,再了解一下這個(gè)
這個(gè)Monitors能夠調(diào)查當(dāng)時(shí)選中app的運(yùn)轉(zhuǎn)情況,現(xiàn)在只需求重視我標(biāo)了123的當(dāng)?shù)亍?/span>
首要這個(gè)Memory就是當(dāng)時(shí)app的內(nèi)存運(yùn)用情況:
發(fā)作一個(gè)當(dāng)時(shí)java堆的.hprof文件,這個(gè)文件反映了當(dāng)時(shí)時(shí)間java堆中內(nèi)存概況,記住這個(gè)玩意有大用!
手動(dòng)進(jìn)行一次gc
這一塊很重要,首要他有兩個(gè)部分,藍(lán)色和灰色。藍(lán)色部分是當(dāng)時(shí)內(nèi)存運(yùn)用巨細(xì),灰色部分是這個(gè)app被約束的最大內(nèi)存巨細(xì)。當(dāng)藍(lán)色部分越來(lái)越大,最終和灰色部分一樣時(shí),闡明我們內(nèi)存運(yùn)用許多了行將內(nèi)存缺乏,此刻會(huì)進(jìn)行一次gc一起將回灰色部分即約束的巨細(xì)進(jìn)步。
肉眼調(diào)查
好了,介紹完這個(gè)東西,我們開(kāi)端著手實(shí)踐。首要翻開(kāi)app,點(diǎn)擊按鈕跳到會(huì)發(fā)作走漏的activity上,再按回來(lái)鍵,然后再次按下按鈕……這樣重復(fù)操作:
與此一起,調(diào)查monitors的memory窗口,會(huì)發(fā)現(xiàn)藍(lán)色部分在每一次敞開(kāi)新activity時(shí)會(huì)增加一部分,這很正常??墒窃诨貋?lái)時(shí),分明activity被“退出”了,可是藍(lán)色部分仍是沒(méi)有改變。重復(fù)幾回之后,藍(lán)色部分一向在增加。也就是說(shuō)當(dāng)時(shí)內(nèi)存越用越多,能夠揣度現(xiàn)已發(fā)作內(nèi)存走漏啦~
主動(dòng)剖析
接下因由android studio來(lái)剖析一下。在重復(fù)幾回上面的操作之后,回來(lái)MainActivity,然后點(diǎn)擊dump java heap按鈕,然后等一會(huì)兒,android studio在為我們dump此刻的horof文件。在成功后,會(huì)主動(dòng)翻開(kāi):
如圖在這個(gè)界面中,我們看最右面有一個(gè)欄叫 Analyzer Tasks,翻開(kāi)它,會(huì)發(fā)現(xiàn)有兩個(gè)選項(xiàng)。我們是來(lái)看activity的內(nèi)存走漏的,那就把那個(gè)查重復(fù)字符串的√去掉。然后點(diǎn)右邊那個(gè)綠色小三角,會(huì)發(fā)現(xiàn)下面Analysis Results欄里邊展現(xiàn)出了當(dāng)時(shí)走漏的Activity引證:
點(diǎn)擊第一個(gè)item,最下方Reference Tree欄中便展現(xiàn)出了詳細(xì)的引證:
一般來(lái)說(shuō),第一個(gè)就是我們發(fā)作走漏的當(dāng)?shù)?。在圖中,this$0的意思是隱式的引證。也就是說(shuō),我們的activity是由于一個(gè)內(nèi)部類(lèi)而發(fā)作了內(nèi)存走漏。
再點(diǎn)擊方才results中第二個(gè)item,看一下下方的reference tree:
能夠看到顯式的有一個(gè)leakCntextRef引證,這闡明我們有一個(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)略,走漏是由于持有了activity引證導(dǎo)致無(wú)法被毀掉,那么只要兩個(gè)挑選:及時(shí)撤銷(xiāo)引證,或許讓這個(gè)引證多待一會(huì),可是該gc的時(shí)分就毀掉。
依據(jù)這個(gè)思路:
我們?cè)诖a中能不必static變量持有contxt就不必,非要用就用weak引證。
關(guān)于內(nèi)部類(lèi),盡量用靜態(tài)內(nèi)部類(lèi),這樣就不會(huì)持有外部類(lèi)引證。如果需求外部類(lèi)引證做一些事,就手動(dòng)賦給一個(gè)weak引證。
關(guān)于匿名內(nèi)部類(lèi),不要圖簡(jiǎn)略便利,真實(shí)不可就乖乖的寫(xiě)成外部類(lèi)。
異步操作,盡量用能夠便利辦理的,比方rxJava,而不是用老古董AsyncTask了。非要用也最好加一個(gè)停止條件,在退出Activity時(shí)就該完畢了。
在用rx時(shí),能夠在subscribe()的時(shí)分獲取到Subscripeion,在不必的時(shí)分手動(dòng)unSubscribe(),或許直接bind()到Activity的生命周期上,比方運(yùn)用RxActivity辦理。
在運(yùn)用handler時(shí),記住在activity的onDestroy()中加上remove()
在獲取到某些資源時(shí),運(yùn)用完記住開(kāi)釋
在用到一些大目標(biāo)比方Bitmap啊什么的,要記住收回
最終,在運(yùn)用各種第三方庫(kù)或許體系效勞的時(shí)分還要記住有注冊(cè)或綁定就要有免除注冊(cè)、解綁定。

