Android 內(nèi)存泄漏

  • 內(nèi)存泄漏的原因
  • 常見的內(nèi)存泄漏與解決方法
  • 檢測內(nèi)存泄漏

認(rèn)識內(nèi)存泄漏

根本原因就是當(dāng)一個(gè)對象理應(yīng)被回收的時(shí)候,因?yàn)樵谀硞€(gè)地方持有該對象的引用糠悼,導(dǎo)致它不能正常被 JVM 回收届榄,而停留在堆內(nèi)存中。
在 Android 中具體的例子大部分是:當(dāng)我們關(guān)閉了一個(gè) Activity/Fragment 時(shí)倔喂,此時(shí) Activity/Fragment 變?yōu)椴豢梢娐撂酰瑑?nèi)存中的實(shí)例也應(yīng)當(dāng)被回收,假如這時(shí)候還有別的對象實(shí)例強(qiáng)引用了 Activity/Fragment 的實(shí)例導(dǎo)致一直無法回收滴劲,則出現(xiàn)了內(nèi)存泄漏攻晒。

關(guān)于 JAVA 的內(nèi)存分配和回收,引用一段:

Java內(nèi)存劃分為棧班挖、堆鲁捏、方法區(qū)等區(qū)域,其中棧保存的是方法的局部變量萧芙,隨方法起隨方法滅给梅,不需要GC;
堆保存所有對象的實(shí)例和數(shù)組双揪,是GC和泄露的重點(diǎn)區(qū)动羽;
方法區(qū)保存的是類信息、常量渔期、靜態(tài)變量等靜態(tài)信息运吓,也需要GC。
堆內(nèi)存的回收中疯趟,判斷對象存活的算法有引用計(jì)數(shù)算法和可達(dá)性分析算法拘哨,引用計(jì)數(shù)算法無法解決對象間循環(huán)引用的問題,虛擬機(jī)通常采用可達(dá)性分析算法信峻。
常見的垃圾回收算法有:標(biāo)記 - 清除法倦青、復(fù)制算法、標(biāo)記 - 整理法盹舞、分代回收算法产镐。
常見的垃圾回收器種類有:Serial隘庄、ParNew、Parallel Scavenge等癣亚。

關(guān)于強(qiáng)引用:
對應(yīng)的常用概念還有軟引用丑掺、弱引用。

強(qiáng)引用特點(diǎn)是 JVM 即使內(nèi)存耗盡也不會(huì)去自動(dòng)回收該對象:

Object o = new Object();//強(qiáng)引用

而軟引用在內(nèi)存不足時(shí)述雾,會(huì)被 JVM 回收:

Staff bean = new Staff();//僅用于創(chuàng)建軟引用的實(shí)例
SoftReference<Staff> staffSR = new SoftReference<Staff>(bean);

//實(shí)際調(diào)用
String StaffID = staffSR.get().getId();

弱引用的使用和軟引用類似吼鱼,不同的是當(dāng) JVM 觸發(fā)了 GC 時(shí),不管當(dāng)前內(nèi)存空間足夠與否绰咽,都會(huì)被回收:

Staff bean = new Staff();//僅用于創(chuàng)建弱引用的實(shí)例
WeakReference<Staff> staffWR = new WeakReference<Staff>(bean);

//實(shí)際調(diào)用
String StaffID = staffSR.get().getId();

內(nèi)存泄漏的實(shí)例

日常的內(nèi)存泄漏其實(shí)追溯到最后還是間接或直接的持有了 Activity/Fragment 的實(shí)例,但是有些確實(shí)防不勝防地粪。一不注意就會(huì)踩雷取募。

  • 單例造成的內(nèi)存泄露

這個(gè)就是老生常談了,因?yàn)閱卫纳芷谕瑧?yīng)用一樣長蟆技,又常有構(gòu)造方法中需要傳入 context 的情況玩敏。
這時(shí)候就要注意,如果傳入了 Activity 的 Context质礼,一不小心就會(huì)使這個(gè)單例持有了 Activity 的實(shí)例而出現(xiàn)內(nèi)存泄漏旺聚。
所以這種情況下,一般用 Application 的 Context 來構(gòu)造單例對象的實(shí)例眶蕉。

((Activity)context).getApplicationContext();

首先要回憶內(nèi)部類的特性砰粹,內(nèi)部類可以訪問外部類的實(shí)例。
對于 JVM 來說造挽,內(nèi)部類和外部類其實(shí)是兩個(gè)不同的類碱璃,正常來說兩個(gè)類之間調(diào)用方法自然是通過兩者的實(shí)例調(diào)用。
而非靜態(tài)內(nèi)部類之所以能訪問外部類的方法饭入,關(guān)鍵在于非靜態(tài)內(nèi)部類會(huì)默認(rèn)隱性的持有外部類的實(shí)例嵌器。
但靜態(tài)的內(nèi)部類則不會(huì)持有外部類的實(shí)例。

相關(guān)的典型代碼:

public class TestActivity extends Activity {

  private final Handler mTestHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      //TODO
    }
  }
}

這時(shí)候的 Handler 是非靜態(tài)的谐丢,所以該實(shí)例持有了 TestActivity 的實(shí)例爽航。
當(dāng)調(diào)用:

//延時(shí)發(fā)送消息
mTestHandler.postDelayed(new Runnable() {
    @Override
    public void run() { 
        //TODO
    }
}, 1000);

如果不清楚 Handler 的原理和源碼的是時(shí)候去補(bǔ)習(xí)一下了。這時(shí)候 Handler 發(fā)送了 Message 實(shí)例乾忱,而這個(gè) Message 實(shí)例引用了 Handler 實(shí)例讥珍,同時(shí) Message 被主線程的 Looper 引用,此時(shí)的引用鏈:
Looper -> Message -> Handler -> Activity
這個(gè)時(shí)候即使調(diào)用 ((Activity)context).finish() Activity 也不能被回收饭耳。

所以上面的情況下要將 Handler 轉(zhuǎn)化成靜態(tài)類即可串述,或者繼承 Handler 將隱性引用的 Activity 實(shí)例改為弱引用:

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

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

    @Override
    public void handleMessage(Message msg) {
        TestActivity activity = mActivity.get();

        if (activity != null) {
                //TODO
        }
    }
}

和這個(gè)問題類似的還有線程造成的泄漏,兩者的原因都是因?yàn)橛昧朔庆o態(tài)的內(nèi)部類:

public class ThreadTestActivity extends Activity {  

    private class MyThread extends Thread {  
        @Override  
        public void run() {  
            super.run();  
            //TODO 
        }  
    } 
  • 關(guān)于非靜態(tài)內(nèi)部類還有另一個(gè)注意點(diǎn)
    如果在非靜態(tài)內(nèi)部類中創(chuàng)建了一個(gè)靜態(tài)的實(shí)例寞肖,這個(gè)操作相當(dāng)于上面說的用 Activity 的實(shí)例來創(chuàng)建單例對象的實(shí)例:靜態(tài)實(shí)例會(huì)一直持有 Activity(外部類) 的實(shí)例纲酗。

  • 資源未關(guān)閉導(dǎo)致

Cursor衰腌、InputStream/OutputStream、File 等資源文件觅赊,如果僅僅是在使用結(jié)束后將引用賦為 null右蕊,而不調(diào)用關(guān)閉的方法,還是有可能會(huì)造成內(nèi)存泄漏吮螺。

特別是 Cursor饶囚,使用數(shù)據(jù)庫時(shí)如果沒有處理好可能會(huì)出現(xiàn) OOM 或 Could not allocate CursorWindow,就是因?yàn)闆]有關(guān)閉導(dǎo)致:

Cursor c ;
//TODO get Cursor
try { 
    c = query();  
    //TODO something
    c.close(); 
    //如果 try 中拋出異常,上面的 cursor.close() 很大可能不會(huì)執(zhí)行
} catch (Exception e) { 

} finally{
    //如果沒有 finally 塊會(huì)容易出錯(cuò)
    if (c != null) {
        c.close();
    }
}

類似的還有廣播鸠补、EventBus 等需要注冊的同時(shí)也要記得在合適的地方注銷萝风。

這次負(fù)責(zé)的其中一個(gè)項(xiàng)目是運(yùn)行在固定的設(shè)備上:Android 4.4.2 的大平板上,所以這個(gè)坑是實(shí)實(shí)在在的踩下去了紫岩。
AlertDialog 中的監(jiān)聽回調(diào)都是靠 Handler 來實(shí)現(xiàn)的:

/*
 * 以下代碼出自 android-23 - android - app - Dialog
 */
public void show() {
    if (mShowing) {
        if (mDecor != null) {
            if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
            }
            mDecor.setVisibility(View.VISIBLE);
        }
        return;
    }
    
    //省略部分代碼

    try {
        mWindowManager.addView(mDecor, l);
        mShowing = true;

        sendShowMessage();
    } finally {
    }
}

@Override
public void dismiss() {
    if (Looper.myLooper() == mHandler.getLooper()) {
        dismissDialog();
    } else {
        mHandler.post(mDismissAction);
    }
}

而我們使用 Dialog 時(shí) .setPositiveButton()规惰、.setNegativeButton().setNeutralButton() 都以非靜態(tài)內(nèi)部類的形式實(shí)現(xiàn)泉蝌,而這些點(diǎn)擊的事件同樣是通過 Handler 回調(diào)歇万,這就會(huì)將這些內(nèi)部類包裝成一個(gè) Message 傳給 Dialog,這個(gè) Message 就強(qiáng)引用了 Activity 的實(shí)例勋陪。
而 Dialog 在使用這些 Message 的時(shí)候會(huì)拷貝一個(gè)對象而不是用原來的對象:

private void sendDismissMessage() {
    if (mDismissMessage != null) {
        // Obtain a new message so this dialog can be re-used
        Message.obtain(mDismissMessage).sendToTarget();
    }
}

也就是說后面使用的是 Message 的拷貝贪磺。所以原來的 Message 從沒有被發(fā)送,因此不會(huì)被回收诅愚,所以永久保存著它的內(nèi)容寒锚,直到發(fā)生垃圾回收。

所以現(xiàn)在的引用鏈:
Thread(CookieSyncManager) -> Message -> AlertDialog$3(OnDismissListener) -> AlertDialog -> Activity

當(dāng)然 5.0 以上已經(jīng)解決了這個(gè)問題违孝,所以這個(gè)案例可以看一看就過壕曼。

檢測內(nèi)存泄漏

不管用什么工具和方法,檢測是否內(nèi)存泄漏的方法都依靠 heap dump 文件等浊。
heap dump 文件是一個(gè)二進(jìn)制文件腮郊,保存了某一時(shí)刻 JVM 堆中對象使用情況,就是生成文件時(shí)的 Java 堆棧的快照筹燕。我們可以選擇用 Heap Analyzer 分析 heap dump 文件轧飞,看哪些對象占用了太多的堆棧空間撒踪,或者哪些對象應(yīng)該被回收卻還在內(nèi)存中过咬。而 Leakcanary 等框架可以幫助我們省去一部分的工作。

  • Leakcanary

build.gradle 中添加如下依賴:

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
 }

在 Application 初始化:

public class MyApplication extends Application {

    private RefWatcher mWatcher;

    @Override 
    public void onCreate() {
        super.onCreate();
        mWatcher = LeakCanary.install(this);//此時(shí)已經(jīng)可以檢測到 Activity 的內(nèi)存泄漏
    }

    public static RefWatcher getWatcher(Context context){
        return ((MyApplication)context.getApplicationContext()).mWatcher;
    }
}

檢測 Fragment :

@Override
public void onDestroy() {
    super.onDestroy();
    //watch() 也可以傳入別的對象實(shí)例來檢測是否泄露
    MyApplication.getWatcher().watch(this);
}

Leakcanary 的 install(application) 相當(dāng)于在 Activity 的 onDestroy() 中調(diào)用 watch(this)制妄。

其原理是在 Activity / Fragment 銷毀后掸绞,先手動(dòng)促發(fā)一次 GC(系統(tǒng) GC 并不會(huì)在銷毀后立刻發(fā)生)
如果 watch(Object bean) 中傳入的 bean 實(shí)例依然存在在內(nèi)存中,則 dump heap 到本地,
dump 完成后啟動(dòng) HeapAnalyzerService 服務(wù)讀取本地 dump下來的文件衔掸,使用 HAHA 庫進(jìn)行分析烫幕。
如果檢測到內(nèi)存泄漏,將結(jié)果返回給 DisplayLeakService 服務(wù)敞映,并且彈窗顯示通知较曼。

  • Android Studio

在不想額外的依賴 Leakcanary 等框架是,利用 AS 自帶的 Monitor 同樣可以檢測內(nèi)存泄漏振愿,只是多了一些步驟捷犹。

Android Monitor -> Monitor

Monitors

從左到右:
Initiate GC // 手動(dòng)觸發(fā) GC。
Jump java heap// 獲取 hprof 分析文件冕末。
Start Allocation Tracking// 開始分配追蹤萍歉。

在 Jump java heap 之前還是要記得觸發(fā)一次 GC。點(diǎn)擊后會(huì)生成一個(gè) 后綴為 hprof 的文件档桃,在 AS 打開:

hprof

右邊的 Analyzer Tasks 打開分析窗口:

Analyzer Tasks

右上角開始翠桦,在 Results 可以看到分析結(jié)果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胳蛮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子丛晌,更是在濱河造成了極大的恐慌仅炊,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件澎蛛,死亡現(xiàn)場離奇詭異抚垄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谋逻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門呆馁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人毁兆,你說我怎么就攤上這事浙滤。” “怎么了气堕?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵纺腊,是天一觀的道長。 經(jīng)常有香客問我茎芭,道長揖膜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任梅桩,我火速辦了婚禮壹粟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宿百。我一直安慰自己趁仙,他們只是感情好洪添,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著幸撕,像睡著了一般薇组。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坐儿,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天律胀,我揣著相機(jī)與錄音,去河邊找鬼貌矿。 笑死炭菌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逛漫。 我是一名探鬼主播黑低,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼酌毡!你這毒婦竟也來了克握?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤枷踏,失蹤者是張志新(化名)和其女友劉穎菩暗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旭蠕,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡停团,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掏熬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佑稠。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖旗芬,靈堂內(nèi)的尸體忽然破棺而出舌胶,到底是詐尸還是另有隱情,我是刑警寧澤疮丛,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布辆琅,位于F島的核電站,受9級特大地震影響这刷,放射性物質(zhì)發(fā)生泄漏婉烟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一暇屋、第九天 我趴在偏房一處隱蔽的房頂上張望似袁。 院中可真熱鬧,春花似錦、人聲如沸昙衅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽而涉。三九已至著瓶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間啼县,已是汗流浹背材原。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留季眷,地道東北人余蟹。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像子刮,于是被迫代替她去往敵國和親威酒。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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

  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題挺峡。內(nèi)存泄漏...
    _痞子閱讀 1,637評論 0 8
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題葵孤。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講橱赠,...
    宇宙只有巴掌大閱讀 2,363評論 0 12
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題尤仍。內(nèi)存泄漏...
    apkcore閱讀 1,221評論 2 7
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了病线,簡單粗俗的講,...
    DreamFish閱讀 791評論 0 5
  • 忙了一天坐下看電視才知道今天是重陽節(jié)鲤嫡,中央臺播出九九重陽節(jié)的晚會(huì)送挑,其中讓我感觸很深的是一個(gè)環(huán)節(jié),給父母化老年妝暖眼,當(dāng)...
    花開墨城閱讀 229評論 0 0