Android - 常見的內(nèi)存泄漏和解決方案總結(jié)

一個無用的對象奖恰,被另一個對象所持有應(yīng)用吊趾,造成該對象在虛擬機的堆中占有的內(nèi)存無法釋放,而導(dǎo)致內(nèi)存空間的浪費瑟啃,這種情況就是內(nèi)存泄漏论泛。

情況1: 單例持有Context

如下代碼:

public class TestManager {

    private static TestManager mInstance;
    private Context mContext;

    public static TestManager getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new TestManager(context);
        }
        return mInstance;
    }

    private TestManager(Context context) {
        mContext = context;
    }
}

分析: 上述代碼中,如果傳入Activity的Context蛹屿,此時TestManager就持有了Activity的引用屁奏,因為mInstance是個靜態(tài)對象,生命周期和整個應(yīng)用一樣長错负,所以如果Activity退出坟瓢,Activity對象占有的內(nèi)存并不能被回收,就會導(dǎo)致內(nèi)存泄漏犹撒。

方案: 此時應(yīng)該傳入全局的上下文Application Context折联,就能避免內(nèi)存泄漏。

情況2: 匿名內(nèi)部類

匿名內(nèi)部類默認持有外部類的引用识颊,如果匿名內(nèi)部類生命周期長于外部類诚镰,就會導(dǎo)致內(nèi)存泄漏,經(jīng)典的例子就是Handler。

private Handler mHandler = new Handler() { 
    @Override 
    public void handleMessage(Message msg) {
        if (msg.what == 1) { 
            // 做相應(yīng)邏輯 
        }
    }
};

分析: 因為Handler在post消息時清笨,會將自己本身作為成員變量保存在發(fā)送的Msg中月杉,目的是方便去識別哪個Handler去處理消息。即message持有handler的引用抠艾,handler持有外部類(可能是Activity)的引用沙合,而message是在MessageQueue中等待Looper輪詢處理的,有可能Activity退出跌帐,而Message并沒有被處理首懈,就會導(dǎo)致Activity間接被Message引用,從而造成內(nèi)存泄漏谨敛。

方案1:使用靜態(tài)內(nèi)部類 + 弱引用組合
因為靜態(tài)內(nèi)部類不持有外部類應(yīng)用究履,所以如果想更新外部類,可以弱引用脸狸。代碼如下:

public static class MyHandler extends Handler {

    private WeakReference<MainActivity> reference;

    public MyHandler (Activity activity) {
        reference = new WeakReference<> (activity);
    }

    public void handlerMessage(Message msg) {
        MainActivity activity = reference.get();
        // 通過activity調(diào)用外部類方法
    }
}

方案2: 在Activity退出時最仑,清空所有Handler發(fā)生出去的消息。

@Override
public void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}
情況3: 廣播注冊后未取消
情況4: Time和TimerTask的使用炊甲,未及時取消

很多情況寫一些定時器或者輪詢?nèi)蝿?wù)時泥彤,會用到Timer,參考如下代碼:

public void timerTest() {
    mTimer = new Timer();
    mTimerTask = new TimerTask {
        @Override
        public void run() {
            // do something
        }
    }
    mTimer.schedule(mTimerTask, 3000,3000);
}

分析: task是個匿名內(nèi)部類卿啡,同樣默認持有外部類引用吟吝,而且task是定時執(zhí)行的,所以Activity退出時颈娜,有可能task并沒有被執(zhí)行剑逃,就會導(dǎo)致內(nèi)存泄漏。

方案: 在Activity退出時官辽,及時取消掉Task蛹磺;

@Override
public void onDestroy() {
    super.onDestroy();

    mTimer.cancel();
    mTimer = null;

    mTimerTask.cancel();
    mTimerTask = null;
}
情況5: 資源未及時關(guān)閉,使用 IO同仆、File 流或者 Sqlite萤捆、Cursor時,通常都會使用緩存俗批,如果不及時關(guān)閉俗或,緩存對象就會一直被占用,導(dǎo)致內(nèi)存泄漏扶镀;
情況6: 動畫未及時取消蕴侣。Activity中使用了屬性動畫Animator,動畫中有引用控件臭觉,而控件引用Activity昆雀,若銷毀時沒有及時cancel辱志,會導(dǎo)致Activity一直被引用,無法回收狞膘,從而造成內(nèi)存泄漏揩懒;
情況7: WebView 的內(nèi)存泄露,因為 WebView 在加載網(wǎng)頁后會長期占用內(nèi)存而不能被釋放挽封,因此我 們在 Activity 銷毀后要調(diào)用它的 destory()方法來銷毀它以釋放內(nèi)存已球。

====================== 續(xù) ========================

情況8: DialogFragment引起的內(nèi)存泄漏。

在寫彈框時辅愿,官方推薦使用DialogFragment智亮,但是在彈框消失時,會報DialogFragment內(nèi)存泄漏点待,且引用鏈的上一級是onCancelListener阔蛉,LeakCanary報錯如下:

┬───
│ GC Root: Java local variable
│
├─ android.os.HandlerThread thread
│    Leaking: UNKNOWN
│    Thread name: 'OkHttpProfiler'
│    ↓ HandlerThread.<Java Local>
│                    ~~~~~~~~~~~~
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    ↓ Message.obj
│              ~~~
├─ androidx.fragment.app.DialogFragment$2 instance
│    Leaking: UNKNOWN
│    Anonymous class implementing android.content.DialogInterface$OnCancelListener
│    ↓ DialogFragment$2.this$0
│                       ~~~~~~
╰→ ***.PrivacyPolicyDialogFragment instance
      Leaking: YES (ObjectWatcher was watching this because 
      ***.PrivacyPolicyDialogFragment

分析: DialogFragment被泄漏,引用鏈上一條在于onCancelListener癞埠,看源碼:

// DialogFragment.java
DialogInterface.OnCancelListener mOnCancelListener = new DialogInterface.OnCancelListener() {
        @Override
        public void onCancel(@Nullable DialogInterface dialog) {
            if (mDialog != null) {
                // 匿名內(nèi)部類状原,持有對外部類DialogFragment的引用
                DialogFragment.this.onCancel(mDialog);
            }
        }
    };

// DialogFragment.java
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    // 省略代碼
    mDialog.setOnCancelListener(mOnCancelListener);
    mDialog.setOnDismissListener(mOnDismissListener);
}

//Dialog.java
public void setOnCancelListener(@Nullable OnCancelListener listener) {
    // 省略代碼
    if (listener != null) {
        /**
         * 劃重點: listener帶有DialogFragment的引用,此時創(chuàng)建mCancelMessage苗踪,
         * 使用obtainMessage方法颠区,在對象池中獲取Message,并將listener賦值給msg的obj屬性通铲,
         * 所以此時mCancelMessage持有了DialogFragment的引用
         */
        mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
    } else {
        mCancelMessage = null;
    }
}

此時的引用鏈為:
Dialog.mCancelMessage -> DialogFragment. mCancelListener -> DialogFragment 毕莱。
所以只能是mCancelMessage泄漏了,才能引發(fā)DialogFragment泄漏测暗。

根據(jù)leakCanary截圖顯示央串,是HandlerThread持有了Message。
問:mCancelMessage是如何被HandlerThread持有的碗啄?
答:mCancelMessage被創(chuàng)建之前。

看看劃重點的地方稳摄,Message是在對象池中拿到的稚字,在Message對象被Handler處理之后,會被recycle厦酬,放入對象池胆描。但是,如果此時是最后一個Message仗阅,由于Looper獲取Message的方法昌讲,是一個阻塞方法,Looper會一直阻塞减噪,并持有msg的引用短绸。
即:
阻塞狀態(tài)的Looper车吹,一定會持有最后一個Message的引用

看代碼:

// Looper.java
public static void loop() {
    //省略代碼
    for (;;) {
        /**
         * 如果沒有消息,next()方法會一直阻塞在這里醋闭,而此時上一個msg窄驹,
         * 會一直被Looper持有,Looper被HandlerThread持有
         */
         Message msg = queue.next(); // might block
    }
    // 省略代碼
}

所以证逻,及時Message被回收放入了對象池乐埠,也會被HandlerThread持有。此時的引用鏈為:
HanlderThread -> Looper -> mCancelMessage ->DialogFragment. mCancelListener -> DialogFragment
一切都真相大白了囚企。

方案:等等哈

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丈咐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子龙宏,更是在濱河造成了極大的恐慌棵逊,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烦衣,死亡現(xiàn)場離奇詭異歹河,居然都是意外死亡,警方通過查閱死者的電腦和手機花吟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門秸歧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人衅澈,你說我怎么就攤上這事键菱。” “怎么了今布?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵经备,是天一觀的道長。 經(jīng)常有香客問我部默,道長侵蒙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任傅蹂,我火速辦了婚禮纷闺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘份蝴。我一直安慰自己犁功,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布婚夫。 她就那樣靜靜地躺著浸卦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪案糙。 梳的紋絲不亂的頭發(fā)上限嫌,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天靴庆,我揣著相機與錄音,去河邊找鬼萤皂。 笑死撒穷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裆熙。 我是一名探鬼主播端礼,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼入录!你這毒婦竟也來了蛤奥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤僚稿,失蹤者是張志新(化名)和其女友劉穎凡桥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚀同,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡缅刽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蠢络。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衰猛。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刹孔,靈堂內(nèi)的尸體忽然破棺而出啡省,到底是詐尸還是另有隱情,我是刑警寧澤髓霞,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布卦睹,位于F島的核電站,受9級特大地震影響方库,放射性物質(zhì)發(fā)生泄漏结序。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一纵潦、第九天 我趴在偏房一處隱蔽的房頂上張望笼痹。 院中可真熱鬧,春花似錦酪穿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至涧团,卻和暖如春只磷,著一層夾襖步出監(jiān)牢的瞬間经磅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工钮追, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留预厌,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓元媚,卻偏偏與公主長得像轧叽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子刊棕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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