参考文档
问题背景
公司开发的一款应用,当点击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
,而Dialog
是setCancelable(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
将窗口添加到显示层。mWindowSession
是IWindowSession
对象,
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
方法将会将当前Dialog
的Window
设置为输入事件的顶层。(Android 9 和 Android 11 的updateInputWindowsLw
方法有一定区别).
原因分析
因为Android 11 中 updateInputWindowsLw的调用是异步的,因此虽然子窗口被创建了,但是其子窗口并没有被置为输入源。因此如果快速的按下press 返回键再release , 在onKeyDown
创建了子窗口,但是当release
的时候Dialog
子窗口还没有被设置为输入事件的顶层,因此事件还是直接被传到了MainActivity
,然后走到onBackPressed
调用finish
,MainActivity
销毁。