DialogFragmen引起的內(nèi)存泄露

一、簡(jiǎn)介

DialogFragment是Android3.0之后引入的一種特殊的Fragment,官方建議使用DialogFragment代替Dialog或者AllertDialog來(lái)實(shí)現(xiàn)彈框的功能,因?yàn)樗梢愿玫墓芾鞤ialog的生命周期以及可以更好復(fù)用。

二、使用中遇到內(nèi)存泄露

在使用過(guò)程中,由于業(yè)務(wù)需要對(duì)DialogFragment的dismiss事件進(jìn)行了監(jiān)聽,在DialogFragment展示與消失的時(shí)候,經(jīng)常會(huì)出現(xiàn)LeakCanary檢測(cè)的內(nèi)存泄露問(wèn)題。查看LeakCanary的內(nèi)存泄露引用鏈如下圖所示:


image.png

這里只貼出了一張圖,LeakCanary每次報(bào)出來(lái)的引用鏈并不完全相同,圖上中顯示的是RxJava的ThreadHandler,有的則顯示的是高德地圖的ThreadHandler(amapLocManagerThread),由此猜測(cè)這里并不是主線程的ThreadHandler引起的內(nèi)存泄露,而是第三方庫(kù)中的ThreadHandler引起的內(nèi)存泄露。但總的來(lái)說(shuō)都是HandlerThread中處理的Message引用了NormalTitleBgDialog(DialogFragment)不能被釋放。下面具體分析一下出現(xiàn)這個(gè)問(wèn)題的原因。

三、原因分析

那么Message是怎么引用到DialogFragment的呢?在DialogFragment中搜索一下Message一無(wú)所獲。DialogFragment實(shí)際是Dialog的封裝,在Dialog中搜索Message試試,果然發(fā)現(xiàn)Dialog的Cancle和Dismiss都是通過(guò)Handler進(jìn)行操作的,從這里入手分析一下內(nèi)存泄露的原因:

  1. DialogFragment中的onActivityCreated
    Cancle和Dismiss的監(jiān)聽傳入的是DialogFragment實(shí)現(xiàn)的兩個(gè)接口:DialogInterface.OnCancelListener, DialogInterface.OnDismissListener
 @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mDialog.setOnCancelListener(this);
        mDialog.setOnDismissListener(this);
    }
  1. setOnDismissListener
    這里會(huì)通過(guò)mListenerHandler獲取到一個(gè)mDismissMessage對(duì)象。
    public void setOnDismissListener(@Nullable OnDismissListener listener) {
        if (listener != null) {
            mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
        } else {
            mDismissMessage = null;
        }
    }

   public void setOnCancelListener(@Nullable OnCancelListener listener) {
        if (listener != null) {
            mCancelMessage = mListenersHandler.obtainMessage(CANCEL, listener);
        } else {
            mCancelMessage = null;
        }
    }

其中obtainMessage內(nèi)部是通過(guò)Message.obtain方法得到,而這個(gè)方法會(huì)從消息池中通過(guò)復(fù)用的方式拿到Message。

 public static Message obtain(Handler h, int what, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.obj = obj;

        return m;
    }

 public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

至此,mDismissMessage中的obj屬性引用了DialogFragment。但是Message是怎么被ThreadHandler引用到并且不能被釋放的呢?下面看一下消息循環(huán)的處理過(guò)程是怎么樣的。

3.Looper.loop
Looper.loop()方法在HandlerThread中運(yùn)行。Looper.loop()方法執(zhí)行過(guò)程就是消息的處理過(guò)程,首先從MessageQueue中取出一條消息,然后調(diào)用msg.target.dispatchMessage(msg)分發(fā)處理消息,最后調(diào)用msg.recycleUnchecked()回收消息。當(dāng)MessageQueue中沒(méi)有消息時(shí)queue.next()方法會(huì)阻塞線程。

 public static void loop() {
        final Looper me = myLooper();
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
           msg.target.dispatchMessage(msg);//分發(fā)消息
            msg.recycleUnchecked();//回收消息
        }
    }

 void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = UID_NONE;
        workSourceUid = UID_NONE;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

一些第三方庫(kù)會(huì)創(chuàng)建自己的消息循環(huán)(HandlerThread),當(dāng)這些消息循環(huán)(HandlerThread)中沒(méi)有消息時(shí),消息循環(huán)線程就會(huì)阻塞。從Java的內(nèi)存模型我們知道線程開啟時(shí)會(huì)創(chuàng)建自己獨(dú)有的虛擬機(jī)??臻g,當(dāng)消息循環(huán)發(fā)生阻塞時(shí),方法中的局部變量不能被釋放。MessageQueue中取出的最后一條消息Message是Looper.loop()方法的局部變量,存儲(chǔ)在棧幀的局部變量表中,由于當(dāng)前線程被阻塞而不能被釋放。以上我們知道了第三方庫(kù)的HandlerThread會(huì)引用Message不能釋放,但是第三方庫(kù)的HandlerThread中的Message怎么會(huì)引用到DialogFragment呢? 由于內(nèi)存泄露發(fā)生是在DialogFragment關(guān)閉時(shí),我們看一下DialogFragment的dismiss是怎么處理的。

  1. dismiss()
    Dialog關(guān)閉時(shí)也是通過(guò)發(fā)送消息來(lái)實(shí)現(xiàn)的,這里通過(guò)Message.obtain復(fù)制了一份mDismissMessage,同樣是從消息池中復(fù)用的消息,因此這里是有可能取到已經(jīng)回收的消息的。
   private void sendDismissMessage() {
        if (mDismissMessage != null) {
            // Obtain a new message so this dialog can be re-used
            Message.obtain(mDismissMessage).sendToTarget();
        }
    }

當(dāng)剛Dialog關(guān)閉dismiss時(shí),剛好取出的是已經(jīng)回收的消息,并且這條消息被另一個(gè)線程所引用,此時(shí)的mDimissMessage重新引用了DialogFragment,因此不能被回收,造成內(nèi)存泄露。

總結(jié):下圖更容易說(shuō)明造成內(nèi)存泄露的原因。左圖是線程A中的消息循環(huán),線程A持有被回收到消息池中的消息對(duì)象,右邊是主線程消息循環(huán),Dialog關(guān)閉時(shí)從消息池中復(fù)用的的mDismissMessage被線程A持有,而mDismissMessage又持有DialogFragment,因?yàn)樵斐闪藘?nèi)存泄露。


image.png

四、驗(yàn)證

五、解決方案

  • LeakCanary提供了一種解決方案:建議第三方庫(kù)一直發(fā)送空的消息,保持第三方庫(kù)的消息循環(huán)消息隊(duì)列一直不為空。這種方式只能是提前知道哪個(gè)第三方庫(kù)創(chuàng)建了自己的消息循環(huán),才能向這個(gè)消息循環(huán)中發(fā)送空消息,這并不能覆蓋到所有的第三方創(chuàng)建的消息循環(huán)。而且,不斷的向一個(gè)阻塞線程中發(fā)消息,線程時(shí)刻處于運(yùn)行狀態(tài),占用線程空間資源。因此,此方案對(duì)于客戶端開發(fā)來(lái)說(shuō)并不可行。
  • 不監(jiān)聽Dialog的dimiss和cancle:如果沒(méi)有需求要監(jiān)聽這兩個(gè)方法則可以直接繼承Dialog,放棄使用DialogFragment。因?yàn)镈ialogFragment在onActivityCreate方法中會(huì)注冊(cè)dismiss和cancle的監(jiān)聽。網(wǎng)絡(luò)上有種方案是通過(guò)重寫DialogFragment在onActivityCreate方法中設(shè)置dialog.setOnCancelListener(null)dialog.setOnDismissListener(null)。如下所示,這種方案仍然會(huì)出現(xiàn)內(nèi)存泄露,原因是在super.onActivityCreate()方法中仍然有 mDialog.setOnCancelListener(this)mDialog.setOnDismissListener(this)。此時(shí)獲取的mDismissMessage有可能是消息池中的消息,而這條消息剛好被一個(gè)消息循環(huán)所持有不能釋放。
  override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        dialog.setOnCancelListener(null)
        dialog.setOnDismissListener(null)
    }

 
  • 弱引用的方式:mDismissMessage實(shí)際上引用的是DialogInterface.OnDismissListener,如果把這個(gè)引用改成弱引用,當(dāng)系統(tǒng)gc時(shí)就能夠回收掉DialogFragment了。這里需要注意的是不能直接繼承DialogFragment,因?yàn)槿绻^承的是DialogFragment,當(dāng)重寫onActivityCreate方法時(shí)加上 super.onActivityCreated(savedInstanceState)還會(huì)出現(xiàn)內(nèi)存泄露,如果不加這句話則會(huì)報(bào)錯(cuò)。下面分步說(shuō)明實(shí)現(xiàn)方法:
  1. 重寫DialogFragment
    直接拷貝DialogFragment代碼至LeakDialogFragment類中,放棄實(shí)現(xiàn)DialogInterface.OnCancelListener, DialogInterface.OnDismissListener兩個(gè)接口
public class LeakDialogFragment extends Fragment {

}

@Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

  public void onDestroyView() {
        super.onDestroyView();
    }

  1. 自定義DialogInterface.OnDismissListener和DialogInterface.OnDismissListener
    public static class DialogDismissListener implements DialogInterface.OnDismissListener {
        private WeakReference<LeakDialogFragment> leakDialogFragmentWeakReference;

        public DialogDismissListener(LeakDialogFragment leakDialogFragment) {
            this.leakDialogFragmentWeakReference = new WeakReference<>(leakDialogFragment);
        }

        @Override
        public void onDismiss(DialogInterface dialog) {
            LeakDialogFragment leakDialogFragment = leakDialogFragmentWeakReference.get();
            if(leakDialogFragment!=null){
                leakDialogFragment.onDismissDialog(dialog);
            }
        }
    }

 public static class DialogCancleListener implements DialogInterface.OnCancelListener {
        private WeakReference<LeakDialogFragment> leakDialogFragmentWeakReference;

        public DialogCancleListener(LeakDialogFragment leakDialogFragment) {
            this.leakDialogFragmentWeakReference = new WeakReference<>(leakDialogFragment);
        }

        @Override
        public void onCancel(DialogInterface dialog) {
            LeakDialogFragment leakDialogFragment = leakDialogFragmentWeakReference.get();
            if(leakDialogFragment!=null){
                leakDialogFragment.onCancelDialog(dialog);
            }
        }
    }

  1. onActivityCreated
    在onActivityCreated中設(shè)置自定義的onDismissListener和onCancleListener,并且在onDestroyView時(shí)設(shè)置為空。
 @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mDialogDissmissLitener = new DialogDismissListener(this);
        mDialog.setOnDismissListener(mDialogDissmissLitener);
        mDialogCancleListener = new DialogCancleListener(this);
        mDialog.setOnCancelListener(mDialogCancleListener);
    }

  public void onDestroyView() {
        super.onDestroyView();
        if(mDialogDissmissLitener!=null){
            mDialogDissmissLitener = null;
        }
        if(mDialogCancleListener!=null){
            mDialogCancleListener = null;
        }
    }

總結(jié)

本文從一個(gè)DialogFragment內(nèi)存泄露問(wèn)題出發(fā),通過(guò)分析Dialog的Dismiss的監(jiān)聽實(shí)現(xiàn)方法,找出了引起內(nèi)存泄露的原因。然后重寫DialogFragment,通過(guò)靜態(tài)內(nèi)部類以及弱引用的方式來(lái)解決內(nèi)存泄露問(wèn)題,希望對(duì)于DialogFragment的使用有幫助。

參考:https://medium.com/square-corner-blog/a-small-leak-will-sink-a-great-ship-efbae00f9a0f

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

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

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