DialogFragment 使用出现的内存泄漏

最近忙的我都焦灼了 。这一天天的 。

 

 

DialogFragment 看名字不就是 Dialog + Fragment 吗 ?想起了之前做弹窗在犹豫使用 AlertDialog 还是 Popupwindow 。 Google 大哥官方推荐说使用 DialogFragment 代替 Dialog (在咱们技术界 ,这几乎已经是潜规则了 ,只要吃一个技术那肯定是要代替老一辈的 ,要不然就出不来 )。

 

使用

使用的时候 ,需要把它给简单的封装一下 事件 、调用 、关闭 。毕竟他是有 Fragment 的一些属性 。

public abstract class BaseDialogFragment extends DialogFragment {

    private boolean mBackCancel = true;//默认点击返回键关闭dialog
    private boolean mTouchOutsideCancel = true;//默认点击dialog外面屏幕,dialog关闭

    private ImmersionBar mImmersionBar;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(DialogFragment.STYLE_NORMAL, R.style.PopDialogTheme3);
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(setLayoutId(), container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (isImmersionBarEnabled()) {
            mImmersionBar = ImmersionBar.with(this);
            mImmersionBar.init();
        }
        initView(view);
        initData();
        setListener();
    }

    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        //点击外部消失
        dialog.setCanceledOnTouchOutside(mTouchOutsideCancel);
        dialog.setOnKeyListener(new KeyBackListener());
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mImmersionBar != null) {
            mImmersionBar.destroy();
        }
        if (getDialog() != null && getDialog().isShowing()) {
            getDialog().dismiss();
        }
    }

    /**
     * Sets layout id.
     *
     * @return the layout id
     */
    protected abstract int setLayoutId();

    /**
     * 是否在Fragment使用沉浸式
     *
     * @return the boolean
     */
    protected boolean isImmersionBarEnabled() {
        return false;
    }

    protected boolean setTouchOutsideCancel(){
        return true;
    }
    /**
     * 初始化数据
     */
    protected void initData() {
    }

    /**
     * view与数据绑定
     */
    protected void initView(View view) {
    }

    /**
     * 设置监听
     */
    protected void setListener() {

    }

    /**
     * 设置点击返回键是否关闭dialog
     **/
    public BaseDialogFragment setCancel(boolean canDismiss) {
        this.mBackCancel = canDismiss;
        return this;
    }

    /**
     * 设置点击屏幕外面是否关闭dialog
     **/
    public BaseDialogFragment setCancelOnTouchOutside(boolean canDismiss) {
        this.mTouchOutsideCancel = canDismiss;
        return this;
    }

    public void showDialog(FragmentManager fragmentManager) {
        try {
            fragmentManager.beginTransaction().remove(this).commit();
            String className = this.getClass().getSimpleName();
            this.show(fragmentManager, className);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void dismissDialog(FragmentManager fragmentManager) {
        String className = this.getClass().getSimpleName();
        Fragment prev = fragmentManager.findFragmentByTag(className);
        if (prev != null) {
            DialogFragment df = (DialogFragment) prev;
            df.dismiss();
        }
    }

    /**
     * 监听返回键的类
     **/
    class KeyBackListener implements DialogInterface.OnKeyListener {
        @Override
        public boolean onKey(DialogInterface dialogInterface, int keyCode, KeyEvent keyEvent) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                return !mBackCancel;
            }
            return false;
        }
    }
}


PS:在代码中调用我相信肯定是没有问题的 ,接下来就是在使用的是过程中会遇到的问题 。

 

造成的内存泄漏

内存泄漏就是分配的内存空间没有及时回收导致的。可使用的内存变少,应用变卡,最后内存溢出后应用就会挂掉 。

 

我先上个截图 

DialogFragment 使用出现的内存泄漏_第1张图片

1. 看上面这个内存泄漏原因 ,可以初步定位是跟 Handler Message 相关的 。搜索一下 Message 。

DialogFragment 使用出现的内存泄漏_第2张图片

Cancel 、Dismiss 、Show 命名上可以得到各个 Message 带边的含义 。

2. 接下来看一下他们在哪被赋值 。

 @Deprecated
    protected Dialog(@NonNull Context context, boolean cancelable,
            @Nullable Message cancelCallback) {
        this(context);
        mCancelable = cancelable;
        updateWindowForCancelable();
        mCancelMessage = cancelCallback;
    }
  public void setOnDismissListener(@Nullable OnDismissListener listener) {
        if (mCancelAndDismissTaken != null) {
            throw new IllegalStateException(
                    "OnDismissListener is already taken by "
                    + mCancelAndDismissTaken + " and can not be replaced.");
        }
        if (listener != null) {
            mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
        } else {
            mDismissMessage = null;
        }
    }
 public void setOnShowListener(@Nullable OnShowListener listener) {
        if (listener != null) {
            mShowMessage = mListenersHandler.obtainMessage(SHOW, listener);
        } else {
            mShowMessage = null;
        }
    }

 

3. 从赋值的地方我们可以得到只要设置监听之后 ,就会生成对应的 Message 。为什么 Message 会内存泄漏呢 ?

我们知道内存泄漏是分配的内存空间没有及时回收导致的 ,通俗一点讲就是我们初始化的对象在该回收的时候仍然被其他对象引用着 。OK  OK ,我们在查看一下 Message 的引用链 。

 Message( mDismissMessage 、mShowMessage )都是在 DialogFragment 里面被调用 set 方法的 。如下图

DialogFragment 使用出现的内存泄漏_第3张图片

直接参数是 this (代表着将 DialogFragment 实例引用到 Dialog ),是因为 DialogFragment 实现了接口 OnCancelListener 、OnDismissListener 。我们在会看上面你的代码截图 ,

mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);

参数 listener  就是我们的 DialogFragment 。在此 可以宣布一个很复杂的恋情 ,是酱紫滴 。。 DialogFragment 包含 Dialog 对象 ,Dialog 里面有 mDismissMessage 和 mCancelMessage 对象 ,他俩又有了 DialogFragment 的实例  。

 

DialogFragment 使用出现的内存泄漏_第4张图片

如上图所示 。接下来看下流程 。

当我们调用 DialogFragment  的 dismiss 的时候 。

  public void dismiss() {
        this.dismissInternal(false);
    }


    void dismissInternal(boolean allowStateLoss) {
        if (!this.mDismissed) {
            this.mDismissed = true;
            this.mShownByMe = false;
            if (this.mDialog != null) {
                this.mDialog.dismiss();
            }

            this.mViewDestroyed = true;
            if (this.mBackStackId >= 0) {
                this.getFragmentManager().popBackStack(this.mBackStackId, 1);
                this.mBackStackId = -1;
            } else {
                FragmentTransaction ft = this.getFragmentManager().beginTransaction();
                ft.remove(this);
                if (allowStateLoss) {
                    ft.commitAllowingStateLoss();
                } else {
                    ft.commit();
                }
            }

        }
    }

在看 Dialog 的 dismiss 。

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


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

追踪的到最就是这行代码 , Message.obtain(mDismissMessage).sendToTarget(); 

PS :补充一个小点 ,Message 的 obtain 的重新方法 

public static Message obtain(Message orig) {}  ,这个方法的含义是将原有的消息体作为一个新的参数来发送的 。

 

OK ,找到发送的 Message ,接下来看 Handler

 private static final class ListenersHandler extends Handler {
        private final WeakReference mDialog;

        public ListenersHandler(Dialog dialog) {
            mDialog = new WeakReference<>(dialog);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DISMISS:
                    ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
                    break;
                case CANCEL:
                    ((OnCancelListener) msg.obj).onCancel(mDialog.get());
                    break;
                case SHOW:
                    ((OnShowListener) msg.obj).onShow(mDialog.get());
                    break;
            }
        }
    }

哈哈哈哈哈哈哈嗝 ,沃日   这又跑到 DialogFragment 的 onDismiss 回调了 。  你没看错 ,就是这么绕 。

所以导致了 内存泄漏 ,Message 对象释放不出去 。

解决方法如下

PS: 重写 onActivityCreated 并且修改里面的逻辑 。

  @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        boolean isShow = this.getShowsDialog();
        this.setShowsDialog(false);
        super.onActivityCreated(savedInstanceState);
        this.setShowsDialog(isShow);

        View view = getView();
        if (view != null) {
            if (view.getParent() != null) {
                throw new IllegalStateException(
                        "DialogFragment can not be attached to a container view");
            }
            this.getDialog().setContentView(view);
        }
        final Activity activity = getActivity();
        if (activity != null) {
            this.getDialog().setOwnerActivity(activity);
        }

        if (savedInstanceState != null) {
            Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
            if (dialogState != null) {
                this.getDialog().onRestoreInstanceState(dialogState);
            }
        }

    }

有瑕疵 ,对 有瑕疵

这样之后点击 home 键回到桌面之后重新进入 APP 页面就会 show dialog 。  可怕不 

最终解决方案 

   private boolean mCancelable = true;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        boolean isShow = this.getShowsDialog();
        this.setShowsDialog(false);
        super.onActivityCreated(savedInstanceState);
        this.setShowsDialog(isShow);

        View view = getView();
        if (view != null)
        {
            if (view.getParent() != null)
            {
                throw new IllegalStateException(
                        "DialogFragment can not be attached to a container view");
            }
            this.getDialog().setContentView(view);
        }
        final Activity activity = getActivity();
        if (activity != null)
        {
            this.getDialog().setOwnerActivity(activity);
        }
        this.getDialog().setCancelable(false);
        this.getDialog().getWindow().getDecorView().setOnTouchListener(this);
        this.getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() {
            @Override
            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE){
                    dismiss();
                    return true;
                }
                return false;
            }
        });
        if (savedInstanceState != null)
        {
            Bundle dialogState = savedInstanceState.getBundle("android:savedDialogState");
            if (dialogState != null)
            {
                this.getDialog().onRestoreInstanceState(dialogState);
            }
        }
    }
    public void setCancelable(boolean mCancelable) {
        this.mCancelable = mCancelable;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mCancelable && getDialog().isShowing()) {
            dismiss();
            return true;
        }
        return false;
    }

 

PS:别忘了实现  View.OnTouchListener

分析工具

https://github.com/spuermax/leakcanary

 

你可能感兴趣的