Android內(nèi)存管理及內(nèi)存泄漏分析(二)

4、常見內(nèi)存泄漏

這是一個(gè)老生常談的一個(gè)問題了追城,但我還是先對(duì)Java中的內(nèi)存泄漏做一個(gè)定義:

Java中的內(nèi)存泄漏就是存在一些被分配的對(duì)象范咨,這些對(duì)象有下面兩個(gè)特點(diǎn)故觅,首先,這些對(duì)象是可達(dá)的湖蜕,即在有向圖中逻卖,存在通路可以與其相連;其次昭抒,這些對(duì)象是無用的评也,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件灭返,這些對(duì)象就可以判定為Java中的內(nèi)存泄漏盗迟,這些對(duì)象不會(huì)被GC所回收,然而它卻占用內(nèi)存熙含。

在C++中罚缕,內(nèi)存泄漏的范圍更大一些。有些對(duì)象被分配了內(nèi)存空間怎静,然后卻不可達(dá)邮弹,由于C++中沒有GC黔衡,這些內(nèi)存將永遠(yuǎn)收不回來。在Java中腌乡,這些不可達(dá)的對(duì)象都由GC負(fù)責(zé)回收盟劫,因此程序員不需要考慮這部分的內(nèi)存泄露。

對(duì)于Java中的內(nèi)存泄漏与纽,我總結(jié)為三點(diǎn):static侣签、線程 和 系統(tǒng)(外部)資源申請(qǐng)。

對(duì)于JVM內(nèi)部而言急迂,它的垃圾回收機(jī)制真的做的非常不錯(cuò)影所,我總覺得Java的出現(xiàn)是程序員的一次體力解放,大家再也不用去關(guān)心那個(gè)讓RD們睡不好覺的指針問題僚碎;而JVM內(nèi)部的內(nèi)存泄漏猴娩,追究起原因來,我覺得(瞎估的)90%是static靜態(tài)變量引起的听盖、10%是while(true)的線程造成的胀溺,大家想一下,我們平時(shí)發(fā)現(xiàn)的內(nèi)存泄漏皆看,是不是大都是注冊(cè)了監(jiān)聽沒釋放仓坞,或者是聲明了一個(gè)大對(duì)象的static為了方便傳遞數(shù)據(jù),結(jié)果忘了置空腰吟;對(duì)于static這樣的變量无埃,我們聲明時(shí)要非常小心,我的意見是毛雇,不是非常必要的情況下嫉称,能不用盡量不要用。

外部資源申請(qǐng)一般指的是打開一些文件灵疮、設(shè)備织阅、數(shù)據(jù)庫(kù)等,這些系統(tǒng)都會(huì)給我們提供一些開銷震捣,如果我們沒能及時(shí)關(guān)閉掉這些設(shè)備荔棉,則會(huì)造成不必要的內(nèi)存開銷。

Android上的內(nèi)存泄漏會(huì)更具體一些蒿赢,有些也會(huì)很隱蔽润樱,我們來具體分析一下,這也是每個(gè)Android程序員面試都會(huì)考到的一個(gè)題羡棵。

(1)activity泄漏

這是我們平時(shí)最關(guān)心的泄漏壹若,因?yàn)锳ctivity在Android的四大組件中持的有資源最多,一個(gè)Activity沒回收,會(huì)導(dǎo)致它里面的無數(shù)個(gè)View都無法被回收到店展。

<1>在需要Context的地方傳入Activity养篓,導(dǎo)致被靜態(tài)變量持有,這種情況大家應(yīng)該碰到的很多赂蕴。

public class AppManager {

private static AppManager instance;

private Context context;

private AppManager(Context context) {

this.context = context;

}

public static synchronized AppManager getInstance(Context context) {

if (instance == null) {

instance = new AppManager(context);

}

return instance;

}

....

// 使用時(shí)

AppManager am = AppManager.getInstance(activity);

上面這種情況觉至,activity就被靜態(tài)變量instance持有了,除非appmanager這個(gè)單例釋放了睡腿。

這種情況有兩個(gè)解決辦法:

1、AppManager am = AppManager.getInstance(activity.getApplicationContext()); // 不建議

2峻贮、private AppManager(Context context) {

this.context = context.getApplicationContext();

}

建議使用第二種方法席怪,因?yàn)檫@是從根上進(jìn)行的解決,而第一種方法是需要調(diào)用者來保證解決纤控,這種方法很不靠譜挂捻,而且從嚴(yán)格意義上說,調(diào)用都使用的根本沒錯(cuò)船万,他傳進(jìn)去的Activity就是一個(gè)Context刻撒。

<2>靜態(tài)變量Activity

這是Activity使用的大忌。

class MyActivity extends Activity {

public static Activity mActivity;

protected void onCreate(...) {

....

mActivity = this耿导;

}

}

這種寫法一般都是程序員偷懶声怔,想通過static的屬性快速獲取一個(gè)Activity的實(shí)例,但這種情況下一般使用的人不知道什么時(shí)候去把mActivity置空舱呻,很容易泄漏醋火。

<3>Static view或static Drawable

class MyActivity extends Activity {

public static View mView;

public static Drawable mDrawable;

}

這個(gè)跟<2>有點(diǎn)相似,都是程序員偷懶干出來的事情箱吕,只要這個(gè)view是這個(gè)activity中創(chuàng)出來的或者用activit inflate進(jìn)來的芥驳,那View中的context就是Activity,所以view在茬高,Activity就不會(huì)被回收兆旬;Drawable這個(gè)實(shí)際上是Android 2.3及以前的一個(gè)Bug,我們看代碼:

//View.setBackground方法:

publicvoidsetBackgroundDrawable(Drawable background) {

....

background.setCallback(this);

.......

}

// Drawable中的setCallback方法

public final void setCallback(Callback cb) {

mCallback = cb;

<4>非static的內(nèi)部類

直接看代碼

非靜態(tài)內(nèi)部類

非靜態(tài)內(nèi)部類怎栽,只要不在賦值給靜態(tài)變量丽猬,在類的內(nèi)部使用起來還是非常方便的,因?yàn)樗梢灾苯诱{(diào)用外部類的變量和方法婚瓜。但如果我們?cè)陬惖耐獠咳ew 一個(gè)這樣的對(duì)象宝鼓,我們應(yīng)該怎么寫呢?

MainActivity.TestResource inner = (new MainActivity()).new TestResource();

從這里可以看出巴刻,一個(gè)非靜態(tài)的內(nèi)部類對(duì)象是必須依附一個(gè)外部類對(duì)象存在的愚铡,這個(gè)時(shí)候如果內(nèi)部類對(duì)象被靜態(tài)變量持有,或者被傳出去注冊(cè)在哪里,就會(huì)導(dǎo)致外部類沥寥,比如這里的Activity無法回收碍舍。

<5>匿名內(nèi)部類,我們最容易忽略的泄漏

Thread造成的匿名內(nèi)部類泄漏

再看一種:

AppManager.getInstance(mContext).registerStateChangedListener(new ? ? AppStateChangedListener() {

....

});

上面一種是線程造成的泄漏邑雅,線程不停片橡,資源不釋放;后一個(gè)是單例造成的淮野;這兩種匿名內(nèi)部類沒有變量持有捧书,基本是必泄漏的。

<6>Handler泄漏

還是先看代碼

內(nèi)部匿名Handler造成的泄漏

我們?nèi)タ匆幌耯andler的源碼找一下原因:

Message的target

每個(gè)Message在放入looper里面時(shí)骤星,都會(huì)為這個(gè)message指定一個(gè)target经瓷,而這個(gè)target就是Handler,如果這個(gè)handler是一個(gè)內(nèi)部類洞难,就會(huì)造成對(duì)應(yīng)的外部類泄漏舆吮。

我們看一下這個(gè)問題在網(wǎng)上的解決辦法:

弱引用法解決

官方提供的方法解決(用的不對(duì),不要參考)

思考兩個(gè)問題:弱引用是解決內(nèi)存泄漏的首先方法嗎队贱?這里使用的官方的方法會(huì)出什么問題色冀?

(2)注冊(cè)和反注冊(cè)

平時(shí)往單例中注冊(cè)一些監(jiān)聽,正常都要在適當(dāng)?shù)臅r(shí)候進(jìn)行反注冊(cè)柱嫌,除非這個(gè)監(jiān)聽是要伴隨著整個(gè)進(jìn)程的生命周期锋恬,這個(gè)比較容易理解,也是靜態(tài)持有導(dǎo)致编丘。

另一種注冊(cè)和反注冊(cè)的情況是receiver,Receiver正常是注冊(cè)到系統(tǒng)中了瘪吏,那到底被誰持有了呢

被loadedApk持有receiver

關(guān)于loadedApk這個(gè)類是在ActivityThread中初始化的癣防,具體它的作用,可以在網(wǎng)上查找一下掌眠。

對(duì)于注冊(cè)到localBroadcastManager中的Receiver蕾盯,就更簡(jiǎn)單了,因?yàn)檫@個(gè)receiver在app內(nèi)部使用蓝丙,所以它就是一個(gè)類似往單例中注冊(cè)listener的形式级遭,必須反注冊(cè)的。

(3)資源對(duì)象沒關(guān)閉造成的內(nèi)存泄露

這種情況的內(nèi)存泄漏渺尘,就是我們開始說的向系統(tǒng)申請(qǐng)資源后沒釋放的情況挫鸽,常見的是流和數(shù)據(jù)庫(kù)未關(guān)閉,對(duì)于這種情況的細(xì)節(jié)就不做代碼分析了鸥跟,我的理解是:

linux對(duì)于每個(gè)設(shè)備等都是以文件來對(duì)待的丢郊,所以不管是文件還是設(shè)備控漠,在打開時(shí)苏携,系統(tǒng)都會(huì)為它創(chuàng)建一定的buffer,這個(gè)buffer是要占用內(nèi)存空間的,如果沒有關(guān)閉對(duì)應(yīng)的流形导,這個(gè)buffer空間是一直被占用的捕透。

(4)Bitmap的recycle不調(diào)會(huì)導(dǎo)致泄漏嗎?

這一項(xiàng)是打了問號(hào)的论颅,即到底Bitmap會(huì)不會(huì)造成內(nèi)存泄漏呢朴下?我們來一點(diǎn)點(diǎn)分析:

對(duì)于Bitmap的recycle()方法需不需要調(diào)用,網(wǎng)上的說法一般是這樣的:

bitmap 2.3以前調(diào)角虫,以后不調(diào)

那沾谓,如果在2.3及以下,不顯式的調(diào)用recycle()戳鹅,是不是就內(nèi)存泄漏了呢搏屑?

我覺得不會(huì),因?yàn)楣俜綄?duì)recycle()的解釋里面粉楚,從沒說必須要調(diào),只是推薦亮垫。

官方2.3及以下圖片內(nèi)存管理的說明

2.3源碼中對(duì)recycle()的說明

從這兩份文檔來看模软,官方的意思應(yīng)該是,這是一個(gè)高級(jí)調(diào)用饮潦,平時(shí)是不需要顯式調(diào)用的燃异,gc回回收這部分內(nèi)存的,但2.3及以下继蜡,如果你確認(rèn)一個(gè)bitmap的確不用了回俐,還是調(diào)一下recycle比較好。

有點(diǎn)把人搞糊涂了稀并,一般大家都認(rèn)為2.3以下bitmap內(nèi)存是native的堆中仅颇,gc收集不到,所以會(huì)引發(fā)一些OOM碘举,但文檔里又說GC會(huì)收集這些內(nèi)存忘瓦,讓我們不用擔(dān)心,到底是怎么一會(huì)事兒呢引颈?

我們還是看源碼吧耕皮,看源碼能解決我們所有的疑惑,每次去看源碼時(shí)蝙场,總能想來來Linus的那句話凌停,好像是"Talk is cheap. Show me the code.",還有"Read the Fucking Source Code"售滤。

Bitmap的源碼

bitmap構(gòu)造函數(shù)中初始化這樣一個(gè)對(duì)象

nativeDestructor方法中釋放內(nèi)存

源碼地址:

https://android.googlesource.com/platform/frameworks/base.git/+/7f9f99ea11051614a7727dfb9f9578b518e76e3c/graphics/java/android/graphics/Bitmap.java

https://android.googlesource.com/platform/frameworks/base/+/android-2.2.1_r2/core/jni/android/graphics/Bitmap.cpp

https://chromium.googlesource.com/chromium/src/+/ae2c20f398933a9e86c387dcc465ec0f71065ffc/skia/sgl/SkBitmap.cpp

到了這里罚拟,大家可以再回過頭思考一個(gè)問題,為什么2.3及以下的bitmap內(nèi)存也不會(huì)泄露,可大家還總是會(huì)說2.3的圖片分配在native舟舒,容易造成OOM呢拉庶?

5、MAT分析內(nèi)存泄漏

對(duì)于內(nèi)存問題的分析秃励,AndroidStudio也提供了dump工具氏仗,但功能與mat比起來還是要弱很多,所以我平時(shí)還是習(xí)慣使用MAT來進(jìn)行分析夺鲜。

打開 DDMS 工具皆尔,在左邊 Devices 視圖頁(yè)面選中“Update Heap”圖標(biāo),然后在右邊切換到 Heap 視圖币励,點(diǎn)擊 Heap 視圖中的“Cause GC”按鈕慷蠕,到此為止需檢測(cè)的進(jìn)程就可以被監(jiān)視。

ddms

Heap視圖中部有一個(gè)Type叫做data object食呻,即數(shù)據(jù)對(duì)象流炕,也就是我們的程序中大量存在的類類型的對(duì)象。在data object一行中有一列是“Total Size”仅胞,其值就是當(dāng)前進(jìn)程中所有Java數(shù)據(jù)對(duì)象的內(nèi)存總量每辟,一般情況下,這個(gè)值的大小決定了是否會(huì)有內(nèi)存泄漏干旧∏郏可以這樣判斷:

進(jìn)入某應(yīng)用,不斷的操作該應(yīng)用椎眯,同時(shí)注意觀察data object的Total Size值挠将,正常情況下Total Size值都會(huì)穩(wěn)定在一個(gè)有限的范圍內(nèi),也就是說由于程序中的的代碼良好编整,沒有造成對(duì)象不被垃圾回收的情況舔稀。

所以說雖然我們不斷的操作會(huì)不斷的生成很多對(duì)象,而在虛擬機(jī)不斷的進(jìn)行GC的過程中掌测,這些對(duì)象都被回收了镶蹋,內(nèi)存占用量會(huì)會(huì)落到一個(gè)穩(wěn)定的水平;反之如果代碼中存在沒有釋放對(duì)象引用的情況赏半,則data object的Total Size值在每次GC后不會(huì)有明顯的回落贺归。隨著操作次數(shù)的增多Total Size的值會(huì)越來越大,直到到達(dá)一個(gè)上限后導(dǎo)致進(jìn)程被殺掉断箫。

MAT分析hprof來定位內(nèi)存泄露的原因所在

這是出現(xiàn)內(nèi)存泄露后使用MAT進(jìn)行問題定位的有效手段拂酣。

A)Dump出內(nèi)存泄露當(dāng)時(shí)的內(nèi)存鏡像hprof,分析懷疑泄露的類:

dump內(nèi)存

注意:這里dump出來的hprof文件仲义,要想直接查看婶熬,是需要在eclipse中安裝mat插件的剑勾;這也帶來一個(gè)問題,要想方便查看赵颅,是要打開eclipse的虽另,但eclipse與androidstudio是不兼容的,打開了一個(gè)饺谬,另一個(gè)的adb就連不上捂刺,這塊的確比較麻煩。

B)使用OQL募寨,查詢內(nèi)存中的對(duì)象:

使用OQL

我們?cè)诓樵儍?nèi)存泄漏時(shí)族展,一般優(yōu)先是看Activity,它持有的內(nèi)存是四大組件中最多的拔鹰,也是我們平時(shí)最容易出現(xiàn)的內(nèi)存泄漏仪缸,為了快速查找出這類的對(duì)象,我們可以使用OQL來寫列肢。

C)分析這些持有引用的對(duì)象的GC路徑

查詢引用關(guān)系

D)逐個(gè)分析每個(gè)對(duì)象的GC路徑是否正常

分析引用持有路徑

從這個(gè)路徑可以看出是一個(gè)antiRadiationUtil工具類對(duì)象持有了MainActivity的引用導(dǎo)致MainActivity無法釋放恰画。此時(shí)就要進(jìn)入代碼分析此時(shí)antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context導(dǎo)致節(jié)目退出后MainActivity無法銷毀,那一般都屬于內(nèi)存泄露了)瓷马。

E)其它的使用:分析持有此類對(duì)象引用的外部對(duì)象

查詢外部引用拴还。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市决采,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坟奥,老刑警劉巖树瞭,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爱谁,居然都是意外死亡晒喷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門访敌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凉敲,“玉大人,你說我怎么就攤上這事寺旺∫ィ” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵阻塑,是天一觀的道長(zhǎng)蓝撇。 經(jīng)常有香客問我,道長(zhǎng)陈莽,這世上最難降的妖魔是什么渤昌? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任虽抄,我火速辦了婚禮,結(jié)果婚禮上独柑,老公的妹妹穿的比我還像新娘迈窟。我一直安慰自己,他們只是感情好忌栅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布车酣。 她就那樣靜靜地躺著,像睡著了一般狂秘。 火紅的嫁衣襯著肌膚如雪骇径。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天者春,我揣著相機(jī)與錄音破衔,去河邊找鬼。 笑死钱烟,一個(gè)胖子當(dāng)著我的面吹牛晰筛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拴袭,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼读第,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了拥刻?” 一聲冷哼從身側(cè)響起怜瞒,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎般哼,沒想到半個(gè)月后吴汪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒸眠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年漾橙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片楞卡。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡霜运,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒋腮,到底是詐尸還是另有隱情淘捡,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布池摧,位于F島的核電站案淋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏险绘。R本人自食惡果不足惜踢京,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一誉碴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓣距,春花似錦黔帕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至逻杖,卻和暖如春奋岁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荸百。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工闻伶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人够话。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓蓝翰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親女嘲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子畜份,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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

  • 4、常見內(nèi)存泄漏 這是一個(gè)老生常談的一個(gè)問題了欣尼,但我還是先對(duì)Java中的內(nèi)存泄漏做一個(gè)定義: Java中的內(nèi)存泄漏...
    編程小豬閱讀 910評(píng)論 1 2
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題爆雹。內(nèi)存泄漏...
    _痞子閱讀 1,636評(píng)論 0 8
  • 我希望通過這篇文章能夠把Android內(nèi)存相關(guān)的基礎(chǔ)和大部分內(nèi)存相關(guān)問題如:溢出、泄漏愕鼓、圖片等等產(chǎn)生的都講解清楚钙态,...
    Cactus_b245閱讀 7,154評(píng)論 6 82
  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了拒啰,簡(jiǎn)單粗俗的講驯绎,...
    DreamFish閱讀 791評(píng)論 0 5
  • 內(nèi)存管理的目的就是讓我們?cè)陂_發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題完慧。內(nèi)存泄漏大家都不陌生了谋旦,簡(jiǎn)單粗俗的講,...
    宇宙只有巴掌大閱讀 2,363評(píng)論 0 12