AlertDialog 引發的內存泄露

追根溯源

在咱們使用AlertDialog時,標準的寫法以下:bash

AlertDialog.Builder builder = new AlertDialog.Builder(this)
        .setPositiveButton("confirm", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                login();
            }
        });
builder.create().show();
複製代碼

但這樣子的寫法是存在內存泄露的,具體分析以下:ide

  • 一、以上代碼在非靜態內部類OnClickListener方法中引用着Activity(activity.this.login()) OnClickListener→ Activiity
public void setButton(int whichButton, CharSequence text,
        DialogInterface.OnClickListener listener, Message msg) {
    if (msg == null && listener != null) {
        msg = mHandler.obtainMessage(whichButton, listener);
    }
    switch (whichButton) {
        case DialogInterface.BUTTON_POSITIVE:
            mButtonPositiveText = text;
            mButtonPositiveMessage = msg;
            break;
        case DialogInterface.BUTTON_NEGATIVE:
            mButtonNegativeText = text;
            mButtonNegativeMessage = msg;
            break;
        case DialogInterface.BUTTON_NEUTRAL:
            mButtonNeutralText = text;
            mButtonNeutralMessage = msg;
            break;
        default:
            throw new IllegalArgumentException("Button does not exist");
    }
}
複製代碼
  • 二、從以上源碼中能夠看到,在AlertDialog構建過程當中傳入的參數 int whichButton, OnClickListener listener都包裝成了msg來處理,這樣子就形成了msg對listener的引用 msg→ OnClickListener
private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        final Message m;
        if (v == mButtonPositive && mButtonPositiveMessage != null) {
            m = Message.obtain(mButtonPositiveMessage);
        } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
            m = Message.obtain(mButtonNegativeMessage);
        } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
            m = Message.obtain(mButtonNeutralMessage);
        } else {
            m = null;
        }
        if (m != null) {
            m.sendToTarget();
        }
        // Post a message so we dismiss after the above handlers are executed
        mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface)
                .sendToTarget();
    }
};
複製代碼
  • 三、以上源碼中,在處理事件響應時,Dialog從消息隊列中再次obtain一個Message實例,複製給m進行發送,Message m也會在Dialog銷燬時跟着銷燬,並無發現產生內存泄露的時機.oop

  • 四、那什麼狀況下會產生內存泄露呢?ui

(1)Message是任何線程共用的,HandlerThread中,Looper會不停的從阻塞隊列MessageQueue中取Message進行處理.當沒有可消費Message對象時,就會開始阻塞,而此時最後一個被取出的Message就會被本地變量引用,一直不會釋放引用,除非有新的messagethis

(2)Dialog從消息隊列中可能會恰巧取到一個「仍然被某個阻塞中的HandlerThread本地變量引用的Message實例」,代碼msg = mHandler.obtainMessage(whichButton, listener),把listener賦給Message的obj,並一直保存在Dialog實例中 如此產生引用: Thread → Mesage → Listener → Dialog → Activity. 當Activity關閉時,Thread仍然引用着Activity, 這樣內存泄漏就發生了.spa

解決方法

  • 定義一個 DetachClickListener 類實現 DialogInterface.OnClickListener 接口
public class DetachClickListener implements DialogInterface.OnClickListener {
    public static DetachClickListener wrap(DialogInterface.OnClickListener delegate) {
        return new DetachClickListener(delegate);
    }
    private DialogInterface.OnClickListener mDelegate;
    private DetachClickListener(DialogInterface.OnClickListener delegate) {
        this.mDelegate = delegate;
    }
    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (mDelegate != null) {
            mDelegate.onClick(dialog, which);
        }
    }
    public void clearOnDetach(Dialog dialog) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            dialog.getWindow()
                    .getDecorView()
                    .getViewTreeObserver()
                    .addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() {
                        @Override
                        public void onWindowAttached() {
                        }
                        @Override
                        public void onWindowDetached() {
                            mDelegate = null;
                        }
                    });
        }
    }
}
複製代碼
  • 使用方式
DetachClickListener clickListener = DetachClickListener.wrap(
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                });
AlertDialog alertDialog = new AlertDialog.Builder(this)
        .setPositiveButton("confirm", clickListener).create();
alertDialog.show();
// 在適當的時機調用該方法,防止內存泄漏
clickListener.clearOnDetach(alertDialog);
複製代碼

以上寫法在Dialog退出後,清除了對DialogInterface.OnClickListener的引用,在中間層截斷, 故在Activity關閉時避免了內存泄露.線程

相關文章
相關標籤/搜索