[关闭]
@SmartDengg 2015-12-24T03:45:23.000000Z 字数 7128 阅读 3651

避免Dialog内存泄露

在java语言中,匿名内部类和非静态内部类持有外部类的强引用。但是被static修饰的变量,不再属于该对象,而属于.class类字节码,生命周期几乎与整个应用是等长的。

Android官方开发组特别喜欢队列的概念,比如我们常用的Handler,Activity栈等都维持了队列的概念,甚至2.3之后的AsyncTask也变成了串行。因此,当我们维护一个handler的时候,应该在组件生命周期结束的时候通过调用.removeMessages()removeCallbacks()移除对应消息,或者使用.removeCallbacksAndMessages(null)移除Looper中的所有延迟消息或回调。当然这只是一段楔子。接下来要说的是Dialog中的回调监听(Callback Listener)。

首先来看一下Dialog中show()方法代码片段:

  1. /**
  2. * Start the dialog and display it on screen. The window is placed in the
  3. * application layer and opaque. Note that you should not override this
  4. * method to do initialization when the dialog is shown, instead implement
  5. * that in {@link #onStart}.
  6. */
  7. public void show() {
  8. if (mShowing) {
  9. if (mDecor != null) {
  10. if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
  11. mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
  12. }
  13. mDecor.setVisibility(View.VISIBLE);
  14. }
  15. return;
  16. }
  17. mCanceled = false;
  18. if (!mCreated) {
  19. dispatchOnCreate(null);
  20. }
  21. onStart();
  22. mDecor = mWindow.getDecorView();
  23. if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
  24. final ApplicationInfo info = mContext.getApplicationInfo();
  25. mWindow.setDefaultIcon(info.icon);
  26. mWindow.setDefaultLogo(info.logo);
  27. mActionBar = new WindowDecorActionBar(this);
  28. }
  29. WindowManager.LayoutParams l = mWindow.getAttributes();
  30. if ((l.softInputMode
  31. & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
  32. WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
  33. nl.copyFrom(l);
  34. nl.softInputMode |=
  35. WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
  36. l = nl;
  37. }
  38. try {
  39. //最重要的一行代码
  40. //将mDecor添加到WindowManager中建立系统间联系,从而管理window的一些状态,如View的添加,移除、更新,以及消息的收集和处理等操作。
  41. mWindowManager.addView(mDecor, l);
  42. mShowing = true;
  43. //发送showMessage消息至handler
  44. sendShowMessage();
  45. } finally {
  46. }
  47. }

调用show()方法之后,我们的dialog就被添加到window中了。

接下来看一下与.dismiss()相关的方法:

  1. /**
  2. * Dismiss this dialog, removing it from the screen. This method can be
  3. * invoked safely from any thread. Note that you should not override this
  4. * method to do cleanup when the dialog is dismissed, instead implement
  5. * that in {@link #onStop}.
  6. */
  7. @Override
  8. public void dismiss() {
  9. if (Looper.myLooper() == mHandler.getLooper()) {
  10. //判断当前线程是否为主线程,如果是则在本线程调用,否则发送消息到主线程
  11. dismissDialog();
  12. } else {
  13. mHandler.post(mDismissAction);
  14. }
  15. }
  16. void dismissDialog() {
  17. if (mDecor == null || !mShowing) {
  18. return;
  19. }
  20. if (mWindow.isDestroyed()) {
  21. Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
  22. return;
  23. }
  24. try {
  25. //立即!立即!立即(重要的事情要说三遍)从窗口中移除mDecor,与.show()方法相对应
  26. mWindowManager.removeViewImmediate(mDecor,);
  27. } finally {
  28. if (mActionMode != null) {
  29. mActionMode.finish();
  30. }
  31. mDecor = null;
  32. mWindow.closeAllPanels();
  33. onStop();
  34. mShowing = false;
  35. //发送一个dismiss消息
  36. sendDismissMessage();
  37. }
  38. }
  39. private void sendDismissMessage() {
  40. if (mDismissMessage != null) {
  41. // Obtain a new message so this dialog can be re-used
  42. Message.obtain(mDismissMessage).sendToTarget();
  43. }
  44. }

既然dialog已经从window中移除了,那么这个发送消息sendDismissMessage()的方法又是干嘛的呢,啊哈,聪明的你一定想到了,处理Listener监听回调(calback)。

不过在这之前要看这样一段代码:

  1. /**
  2. * Set a listener to be invoked when the dialog is dismissed.
  3. * @param listener The {@link DialogInterface.OnDismissListener} to use.
  4. */
  5. public void setOnDismissListener(final OnDismissListener listener) {
  6. if (mCancelAndDismissTaken != null) {
  7. throw new IllegalStateException(
  8. "OnDismissListener is already taken by "
  9. + mCancelAndDismissTaken + " and can not be replaced.");
  10. }
  11. if (listener != null) {
  12. //mDismissMessage对应的是mListenersHandler,所以只需要看看mListenersHandler是如何实现的,就能得出结论了。
  13. mDismissMessage = mListenersHandler.obtainMessage(DISMISS, listener);
  14. } else {
  15. mDismissMessage = null;
  16. }
  17. }

mListenersHandler实现如下:

  1. private static final class ListenersHandler extends Handler {
  2. private WeakReference<DialogInterface> mDialog;
  3. public ListenersHandler(Dialog dialog) {
  4. //弱引用,小技巧。
  5. mDialog = new WeakReference<DialogInterface>(dialog);
  6. }
  7. @Override
  8. public void handleMessage(Message msg) {
  9. switch (msg.what) {
  10. case DISMISS:
  11. ((OnDismissListener) msg.obj).onDismiss(mDialog.get());
  12. break;
  13. case CANCEL:
  14. ((OnCancelListener) msg.obj).onCancel(mDialog.get());
  15. break;
  16. case SHOW:
  17. ((OnShowListener) msg.obj).onShow(mDialog.get());
  18. break;
  19. }
  20. }
  21. }

啊哈,果然是会处理我们的监听回调.onDismiss().onCancel().onShow(),但是依然要遵循队列的概念,至于什么时候处理,还要看handler与looper的调度。不过显然这问题不大,因为view的更新,总是要先于回调的,所以把这些通知消息交给handler+looper处理是正确的处理方式。

那么问题来了这三个点击监听.setPositiveButton().setNegativeButton().setNeutralButton(),也是通过handler发送message实现的:

  1. private Handler mHandler;
  2. private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
  3. @Override
  4. public void onClick(View v) {
  5. final Message m;
  6. if (v == mButtonPositive && mButtonPositiveMessage != null) {
  7. m = Message.obtain(mButtonPositiveMessage);
  8. } else if (v == mButtonNegative && mButtonNegativeMessage != null) {
  9. m = Message.obtain(mButtonNegativeMessage);
  10. } else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
  11. m = Message.obtain(mButtonNeutralMessage);
  12. } else {
  13. m = null;
  14. }
  15. if (m != null) {
  16. m.sendToTarget();
  17. }
  18. // Post a message so we dismiss after the above handlers are executed
  19. mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialog)
  20. .sendToTarget();
  21. }
  22. };
  23. private static final class ButtonHandler extends Handler {
  24. // Button clicks have Message.what as the BUTTON{1,2,3} constant
  25. private static final int MSG_DISMISS_DIALOG = 1;
  26. private WeakReference<DialogInterface> mDialog;
  27. public ButtonHandler(DialogInterface dialog) {
  28. mDialog = new WeakReference<>(dialog);
  29. }
  30. @Override
  31. public void handleMessage(Message msg) {
  32. switch (msg.what) {
  33. case DialogInterface.BUTTON_POSITIVE:
  34. case DialogInterface.BUTTON_NEGATIVE:
  35. case DialogInterface.BUTTON_NEUTRAL:
  36. ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
  37. break;
  38. case MSG_DISMISS_DIALOG:
  39. ((DialogInterface) msg.obj).dismiss();
  40. }
  41. }
  42. }

如果点击PositiveButton、NegativeButton、NeutralButton之后立即调用了.dismiss(),这可就麻烦了,dialog立即从window中移除,因为looper是按照队列消费message的,所以如果这些点击事件的message遇到了某些延迟则还存在于handler中,那么就会遇到文章开头说的那种内存泄露,甚至是NullPointerException等异常。既然找到了问题的所在,那么就很好解决了,只要监听dialog的viewTree,当dialog被windowManager从window中移除的时候,将所设置的listener置空就哦了。

这是我之前的一段代码,已经提交到了update_develop分支上了:

  1. public final class DetachableClickListener implements DialogInterface.OnClickListener {
  2. public static DetachableClickListener wrap(DialogInterface.OnClickListener delegate) {
  3. return new DetachableClickListener(delegate);
  4. }
  5. private DialogInterface.OnClickListener delegateOrNull;
  6. private DetachableClickListener(DialogInterface.OnClickListener delegate) {
  7. this.delegateOrNull = delegate;
  8. }
  9. @Override public void onClick(DialogInterface dialog, int which) {
  10. if (delegateOrNull != null) {
  11. delegateOrNull.onClick(dialog, which);
  12. }
  13. }
  14. @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public void clearOnDetach(Dialog dialog) {
  15. dialog.getWindow()
  16. .getDecorView()
  17. .getViewTreeObserver()
  18. .addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() {
  19. @Override public void onWindowAttached() {
  20. }
  21. @Override public void onWindowDetached() {
  22. DetachableClickListener.this.delegateOrNull = null;
  23. }
  24. });
  25. }
  26. }

使用方法也很简单,只需要包裹一层listener即可:

  1. DetachableClickListener warpClickListener = DetachableClickListener.wrap(new DialogInterface.OnClickListener() {
  2. @Override public void onClick(DialogInterface dialog, int which) {
  3. dialog.dismiss();
  4. }
  5. });
  6. AlertDialog confirmDialog = new AlertDialog.Builder(context)
  7. .setTitle("提示")
  8. .setMessage(content)
  9. .setCancelable(false)
  10. .setInverseBackgroundForced(false)
  11. .setPositiveButton("确定", warpClickListener)
  12. .create();
  13. warpClickListener.clearOnDetach(confirmDialog);
  14. confirmDialog.getWindow().setWindowAnimations(R.style.AnimCenter);
  15. confirmDialog.show();

OK,虽然梳理了好多源码,但是貌似解决了个小bug,我们的程序更加健壮了,我们就是要把它做好,不是吗:)

敬请期待下一篇,我将追溯Android的异步框架历史,从Service + Thread + Callback ——> AsyncTask + Handler ——> LoaderManager + Callback ——> RxJava,避开Android Framework中的一些坑和bug(其实Android官方开组和V包负责人也公开表示过Android并非无bug,也需要开发者的共同努力)。

欢迎交流 :)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注