Android 內(nèi)存泄漏

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

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

根本原因就是當(dāng)一個對象理應(yīng)被回收的時候,因?yàn)樵谀硞€地方持有該對象的引用,導(dǎo)致它不能正常被 JVM 回收,而停留在堆內(nèi)存中。
在 Android 中具體的例子大部分是:當(dāng)我們關(guān)閉了一個 Activity/Fragment 時,此時 Activity/Fragment 變?yōu)椴豢梢?,?nèi)存中的實(shí)例也應(yīng)當(dāng)被回收,假如這時候還有別的對象實(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)存的回收中,判斷對象存活的算法有引用計數(shù)算法和可達(dá)性分析算法,引用計數(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)存耗盡也不會去自動回收該對象:

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

而軟引用在內(nèi)存不足時,會被 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 時,不管當(dāng)前內(nèi)存空間足夠與否,都會被回收:

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í)防不勝防。一不注意就會踩雷。

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

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

((Activity)context).getApplicationContext();

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

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

public class TestActivity extends Activity {

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

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

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

如果不清楚 Handler 的原理和源碼的是時候去補(bǔ)習(xí)一下了。這時候 Handler 發(fā)送了 Message 實(shí)例,而這個 Message 實(shí)例引用了 Handler 實(shí)例,同時 Message 被主線程的 Looper 引用,此時的引用鏈:
Looper -> Message -> Handler -> Activity
這個時候即使調(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
        }
    }
}

和這個問題類似的還有線程造成的泄漏,兩者的原因都是因?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)部類還有另一個注意點(diǎn)
    如果在非靜態(tài)內(nèi)部類中創(chuàng)建了一個靜態(tài)的實(shí)例,這個操作相當(dāng)于上面說的用 Activity 的實(shí)例來創(chuàng)建單例對象的實(shí)例:靜態(tài)實(shí)例會一直持有 Activity(外部類) 的實(shí)例。

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

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

特別是 Cursor,使用數(shù)據(jù)庫時如果沒有處理好可能會出現(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() 很大可能不會執(zhí)行
} catch (Exception e) { 

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

類似的還有廣播、EventBus 等需要注冊的同時也要記得在合適的地方注銷。

這次負(fù)責(zé)的其中一個項(xiàng)目是運(yùn)行在固定的設(shè)備上:Android 4.4.2 的大平板上,所以這個坑是實(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 時 .setPositiveButton()、.setNegativeButton().setNeutralButton() 都以非靜態(tài)內(nèi)部類的形式實(shí)現(xiàn),而這些點(diǎn)擊的事件同樣是通過 Handler 回調(diào),這就會將這些內(nèi)部類包裝成一個 Message 傳給 Dialog,這個 Message 就強(qiáng)引用了 Activity 的實(shí)例。
而 Dialog 在使用這些 Message 的時候會拷貝一個對象而不是用原來的對象:

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

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

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

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

檢測內(nèi)存泄漏

不管用什么工具和方法,檢測是否內(nèi)存泄漏的方法都依靠 heap dump 文件。
heap dump 文件是一個二進(jìn)制文件,保存了某一時刻 JVM 堆中對象使用情況,就是生成文件時的 Java 堆棧的快照。我們可以選擇用 Heap Analyzer 分析 heap dump 文件,看哪些對象占用了太多的堆??臻g,或者哪些對象應(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);//此時已經(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 銷毀后,先手動促發(fā)一次 GC(系統(tǒng) GC 并不會在銷毀后立刻發(fā)生)
如果 watch(Object bean) 中傳入的 bean 實(shí)例依然存在在內(nèi)存中,則 dump heap 到本地,
dump 完成后啟動 HeapAnalyzerService 服務(wù)讀取本地 dump下來的文件,使用 HAHA 庫進(jìn)行分析。
如果檢測到內(nèi)存泄漏,將結(jié)果返回給 DisplayLeakService 服務(wù),并且彈窗顯示通知。

  • Android Studio

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

Android Monitor -> Monitor

Monitors

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

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

hprof

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

Analyzer Tasks

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容