Android 9和9之后系统 在onKeyDown中Dialog.show问题分析(以andoird 11 和android 9代码作为分析)

参考文档

  1. Android KeyEvent 点击事件分发处理流程
  2. InputManagerService原理
  3. Android 源码 输入系统之窗口关联_洪伟的专栏-CSDN博客

问题背景

公司开发的一款应用,当点击back键的时候,会弹出一个Dialog,Dailog 有三个按钮。确定上传取消。这三个按钮意义不大,重点是应用会拦截 back键,只有点击Dialog的【取消】按钮,才能退出应用。但是 在Android 10中,却出现如果点击 back 键太频繁,直接返回了桌面。

而正常的流程应该是,用户点击了back键,会弹出Dialog弹窗,再点击 Dialog 就会关闭弹窗,然后再点击back,弹窗Dialog,如此循环往复,而不会直接退出到桌面。

示例代码

公司的代码大体逻辑如下,可以直接粘贴下面代码去在Android 9 和 10(11)上验证下,频繁点击back键会出现直接退到桌面。

package com.chl.onkeyanalysis;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";
    AlertDialog alertDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getBaseContext();
        createAlertDialog();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        return super.onCreateView(name, context, attrs);
    }

    private void createAlertDialog() {
        alertDialog =
                new AlertDialog.Builder(this)
                        .setCancelable(true)
                        .setTitle("ww")
                        .setMessage("ww")
                        .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                            }
                        })
                        .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                            }
                        })
                        .setOnKeyListener(new DialogInterface.OnKeyListener() {
                            @Override
                            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
                                if (keyCode == KeyEvent.KEYCODE_BACK
                                        && event.getAction() == KeyEvent.ACTION_DOWN) {
                                    Log.i(TAG, "onCancel1 keyBack down: " );
                                } else if (keyCode == KeyEvent.KEYCODE_BACK
                                        && event.getAction() == KeyEvent.ACTION_UP) {
                                    Log.i(TAG, "onCancel1 keyBack up: " );
                                }
                                return false;
                            }
                        })
                        .setOnCancelListener(new DialogInterface.OnCancelListener() {
                            @Override
                            public void onCancel(DialogInterface dialog) {
                            }
                        })
                        .setNeutralButton("上传", new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                            }
                        })
                        .create();
    }

    // 代码1
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            alertDialog.show();
        }
        boolean re = super.onKeyDown(keyCode, event);
        return re;
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        Log.i(TAG, "onBackPressed: ");
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent: ");
        boolean re = super.dispatchTouchEvent(ev);
        return re;
    }

    @Override
    protected void onDestroy() {
        Log.i(TAG, "onDestroy: ");
        super.onDestroy();
    }
}
复制代码

如上代码,onKeyDown 中监听到back键就会显示Dialog,而DialogsetCancelable(true)的,点击back键,其就会退出。

注: 这段 onKeyDown 代码本身其实也是有问题的,既然back键已经被拦截了,就应该返回 false,那样也就不会出现直接退到桌面的问题了。公司之前的人不知道代码为什么这样写的,bug非常好解决,onKeyDown代码中直接return false就OK,但是原因还是需要找的!

问题分析

首先,其实我是很奇怪公司这段之前的代码居然一直能成功拦截Activity,没有退出的。因为一般而言,onKeyDown返回true,不应该就是直接退出吗?
要分析这个问题,首先我们得定位下,一般情况下,按下 back键后,在不拦截back键情况下,Activity如何退出的?
正常的back键按下后,会走到onKeyUp 方法中,而在onKeyUp 方法中会调用 onBackPressed方法。

onBackPressed 中会进行判断,最终调用finish()方法,onBackPressed方法具体调用此处就不分析了,有兴趣的同学可以自己去看代码。

现在记住的一点就是,在实例中的MainActivity 一旦走到 onBackPressed方法就会退出应用了。

接下来,开始我们的正题了。

onKeyUP 会被调用吗

onKeyDown 中,我们 返回的值是true,其实就是告知AMS,不拦截 该事件,所以会走到 onKeyUp代码,看下onKeyUp的实现。

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getApplicationInfo().targetSdkVersion
            >= Build.VERSION_CODES.ECLAIR) {
        // 代码会走到此处
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
    }
    return false;
}
复制代码

显然,如果代码想要走到 onBackPressed处,需要满足两个条件event.isTracking() 以及!event.isCanceled().

让我们在onKeyDown 中加下log.

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    Log.i(TAG, "onKeyUp: event isTracking:"+(event.isTracking())+" isCanceled:"+event.isCanceled());
    return super.onKeyUp(keyCode, event);
}
复制代码
  • 在9.0系统中,运行结果:
MainActivity: onKeyUp: event isTracking:true isCanceled:true
复制代码
  • 10.0系统,运行结果:
MainActivity: onKeyUp: event isTracking:true isCanceled:false
复制代码

在 9.0系统,显然该事件被拦截了,调用了 cancel,而10.0中,则没有调用cancel

9.0事件中,KeyEvent其实是被 AlertDialog 拦截了,而 10.0中却没有被拦截。于是我猜测会不会是Dialog的创建过程中出现了异步操作。(事实上确实就是这个原因!)

接下来,开始分析 AlertDialog的创建过程。

AlertDialog 与 其他 View组件不同,其是一个 子Window。而其依附的Activity则是其父Window.当KeyEvent到来时,子Window 对 事件的处理具有更高的优先级。

AlertDialog 添加到Window的流程(android 11)

AlertDialog.show 最终调用到WindowManagerGlobal.addView, AlertDialog.show不分析具体流程。
AlertDialog.show -> WindowManagerImpl.addView -> WindowManagerGlobal.addView。

WindowManagerGlobal是 WindowManager的方法的真正实现处,其在每个Application 中只有一个

WindowManagerImpl

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
            mContext.getUserId());
}
复制代码

WindowManagerGlobal.addView(以下代码只保留了核心步骤)

@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
        ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
             .....
            // 创建一个ViewRootImpl,ViewRootImpl 实现了一些接口,方便 WindowManagerGlobal 对视图的操作
            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);
            // 将 DecorView 添加到 mViews
            mViews.add(view);
            // 将 ViewRootImpl 对象存到数组中
            mRoots.add(root);
            // 将 LayoutParams参数存入数组中
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                // 最终要的操作
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
              ...
            }
        }
    }
复制代码

此时 Dialog的视图还未显示,最重要的步骤在 root.setView 这个方法中。

接下来看下 ViewRootImpl.setView的实现:

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                mAttachInfo.mDisplayState = mDisplay.getState();
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                ...

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();// 对布局进行测量,绘制
                InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    inputChannel = new InputChannel();
                }
                mForceDecorViewVisibility = (mWindowAttributes.privateFlags
                        & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    adjustLayoutParamsForCompatibility(mWindowAttributes);
                    // 代码1 核心代码
                    res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
                    setFrame(mTmpFrame);
                } catch (RemoteException e) {
                     ...
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }
                ...
            }
        }
    }
复制代码

代码1 处通过mWindowSession.addToDisplayAsUser 将窗口添加到显示层。mWindowSessionIWindowSession 对象,
IWindowSession 对象是一个Binder, 其赋值过程

 public ViewRootImpl(Context context, Display display) {
        this(context, display, WindowManagerGlobal.getWindowSession(),
                false /* useSfChoreographer */);
    }

    public ViewRootImpl(Context context, Display display, IWindowSession session) {
        this(context, display, session, false /* useSfChoreographer */);
    }

    public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        mContext = context;
        mWindowSession = session;
        mDisplay = display;
        ...
    }
复制代码

其传入的是 WindowManagerGlobal.getWindowSession();
WindowManagerGlobal.getWindowSession() 实现过程如下:

 @UnsupportedAppUsage
    public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
                ...
            }
            return sWindowManagerService;
        }
    }

    @UnsupportedAppUsage
    public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                   ...
                    IWindowManager windowManager = getWindowManagerService();
                    sWindowSession = windowManager.openSession(
                            new IWindowSessionCallback.Stub() {
                                @Override
                                public void onAnimatorScaleChanged(float scale) {
                                    ValueAnimator.setDurationScale(scale);
                                }
                            });
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            return sWindowSession;
        }
    }
复制代码

getWindowSession方法显然是调用 IWindowManager.openSession方法,而IWindowManager 也是一个binder对象,IWindowManager.openSession其对应的是WindowManagerService.openSession方法。而openSession方法返回的是一个Session对象 .

// -------------------------------------------------------------
// IWindowManager API
// -------------------------------------------------------------
@Override
public IWindowSession openSession(IWindowSessionCallback callback) {
    return new Session(this, callback);
}

复制代码

Session.java代码如下

class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
    final WindowManagerService mService;
    final IWindowSessionCallback mCallback;

    public Session(WindowManagerService service, IWindowSessionCallback callback) {
        mService = service;
        mCallback = callback;
        ...
    }
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
            Rect outStableInsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
                outInsetsState, outActiveControls, UserHandle.getUserId(mUid));
    }


    @Override
    public int addToDisplayAsUser(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, int userId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
                outInsetsState, outActiveControls, userId);
    }
    ...
复制代码

显然,mWindowSession.addToDisplayAsUser 代码调用的是WindowManagerService.addWindow方法。

WindowManagerService.addWindow代码如下:


public int addWindow(Session session, IWindow client, int seq,
            LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
            Rect outContentInsets, Rect outStableInsets,
            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
            InsetsState outInsetsState, InsetsSourceControl[] outActiveControls,
            int requestUserId) {
            ...
            if (focusChanged) {
                displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,
                        false /*updateInputWindows*/);
            }
            // 核心代码
            displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);

            ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s"
                    + ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5));

            if (win.isVisibleOrAdding() && displayContent.updateOrientation()) {
                displayContent.sendNewConfiguration();
            }

            getInsetsSourceControls(win, outActiveControls);
            ...
        Binder.restoreCallingIdentity(origId);

        return res;
    }
复制代码

终于来到了 android 9 和 其之后版本的差异点了。
updateInputWindowsLw 方法,更新输入窗口。当前增加了输入窗口,当前输入事件也将被更新

android 10 之后scheduleUpdateInputWindows方法代码
InputMonitor.java#scheduleUpdateInputWindows

...
private final UpdateInputWindows mUpdateInputWindows = new UpdateInputWindows();
private class UpdateInputWindows implements Runnable {
        @Override
        public void run() {
            synchronized (mService.mGlobalLock) {
                mUpdateInputWindowsPending = false;
                mUpdateInputWindowsNeeded = false;

                if (mDisplayRemoved) {
                    return;
                }

                // Populate the input window list with information about all of the windows that
                // could potentially receive input.
                // As an optimization, we could try to prune the list of windows but this turns
                // out to be difficult because only the native code knows for sure which window
                // currently has touch focus.

                // If there's a drag in flight, provide a pseudo-window to catch drag input
                final boolean inDrag = mService.mDragDropController.dragDropActiveLocked();

                // Add all windows on the default display.
                mUpdateInputForAllWindowsConsumer.updateInputWindows(inDrag);
            }
        }
    }
void updateInputWindowsLw(boolean force) {
    if (!force && !mUpdateInputWindowsNeeded) {
        return;
    }
    scheduleUpdateInputWindows();
}

private void scheduleUpdateInputWindows() {
    if (mDisplayRemoved) {
        return;
    }

    if (!mUpdateInputWindowsPending) {
        mUpdateInputWindowsPending = true;
        mHandler.post(mUpdateInputWindows);
    }
}
...

复制代码

android 9代码

void updateInputWindowsLw(boolean force) {
      if (!force && !mUpdateInputWindowsNeeded) {
          return;
      }
      mUpdateInputWindowsNeeded = false;

      if (false) Slog.d(TAG_WM, ">>>>>> ENTERED updateInputWindowsLw");

      // Populate the input window list with information about all of the windows that
      // could potentially receive input.
      // As an optimization, we could try to prune the list of windows but this turns
      // out to be difficult because only the native code knows for sure which window
      // currently has touch focus.

      // If there's a drag in flight, provide a pseudo-window to catch drag input
      final boolean inDrag = mService.mDragDropController.dragDropActiveLocked();
      if (inDrag) {
          if (DEBUG_DRAG) {
              Log.d(TAG_WM, "Inserting drag window");
          }
          final InputWindowHandle dragWindowHandle =
                  mService.mDragDropController.getInputWindowHandleLocked();
          if (dragWindowHandle != null) {
              addInputWindowHandle(dragWindowHandle);
          } else {
              Slog.w(TAG_WM, "Drag is in progress but there is no "
                      + "drag window handle.");
          }
      }

      final boolean inPositioning = mService.mTaskPositioningController.isPositioningLocked();
      if (inPositioning) {
          if (DEBUG_TASK_POSITIONING) {
              Log.d(TAG_WM, "Inserting window handle for repositioning");
          }
          final InputWindowHandle dragWindowHandle =
                  mService.mTaskPositioningController.getDragWindowHandleLocked();
          if (dragWindowHandle != null) {
              addInputWindowHandle(dragWindowHandle);
          } else {
              Slog.e(TAG_WM,
                      "Repositioning is in progress but there is no drag window handle.");
          }
      }
      // Add all windows on the default display.
      mUpdateInputForAllWindowsConsumer.updateInputWindows(inDrag);

      if (false) Slog.d(TAG_WM, "<<<<<<< EXITED updateInputWindowsLw");
}
复制代码

可见,在Android 9 上,调用InputMonitor$UpdateInputForAllWindowsConsumer#updateInputWindowsLw 方法是同步的,而android 10及之后上其是异步的。

updateInputWindowsLw 方法将会将当前DialogWindow设置为输入事件的顶层。(Android 9 和 Android 11 的updateInputWindowsLw方法有一定区别).

原因分析

因为Android 11 中 updateInputWindowsLw的调用是异步的,因此虽然子窗口被创建了,但是其子窗口并没有被置为输入源。因此如果快速的按下press 返回键再release , 在onKeyDown创建了子窗口,但是当release的时候Dialog子窗口还没有被设置为输入事件的顶层,因此事件还是直接被传到了MainActivity,然后走到onBackPressed调用finish,MainActivity销毁。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享