1. 问题背景 BadTokenException
在 Service 的 onCreate 方法中 addView,运行以下代码:
WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
View view = LayoutInflater.from(this).inflate(R.layout.custom_dialog, null);
windowManager.addView(view, new WindowManager.LayoutParams());
会出现以下错误:android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
看一下抛出错误的地方,是在 ViewRootImpl 的 setView 方法中。
2. 流程分析
重新梳理一下流程,首先调用了 WindowManagerImpl 的 addView 方法:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyTokens(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
继续调用到 WindowManagerGlobal 的 addView 方法:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
IWindowSession windowlessSession = null;
// If there is a parent set, but we can't find it, it may be coming
// from a SurfaceControlViewHost hierarchy.
if (wparams.token != null && panelParentView == null) {
for (int i = 0; i < mWindowlessRoots.size(); i++) {
ViewRootImpl maybeParent = mWindowlessRoots.get(i);
if (maybeParent.getWindowToken() == wparams.token) {
windowlessSession = maybeParent.getWindowSession();
break;
}
}
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (windowlessSession == null) {
root = new ViewRootImpl(view.getContext(), display);
}
try {
root.setView(view, wparams, panelParentView, userId);
}
}
传入的 params 为 new WindowManager.LayoutParams(),看一下构造方法:
public static final int TYPE_APPLICATION = 2;
/**
* Identifier for this window. This will usually be filled in for
* you.
*/
public IBinder token = null;
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
此时,LayoutParams 的 type 为 TYPE_APPLICATION,token 为 null。
继续看 ViewRootImpl setView 方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mWindowAttributes.copyFrom(attrs);
attrs = mWindowAttributes;
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
}
}
}
调用 Session 的 addToDisplayAsUser 方法,远程调用到 wms,添加 window:
@Override
public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
float[] outSizeCompatScale) {
// WMS
return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,
outAttachedFrame, outSizeCompatScale);
}
调用 WMS 的 addWindow 方法:
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
float[] outSizeCompatScale) {
WindowState parentWindow = null;
// type,客户端创建默认 TYPE_APPLICATION 2
final int type = attrs.type;
ActivityRecord activity = null;
final boolean hasParent = parentWindow != null;
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
if (token == null) {
if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
rootType, attrs.token, attrs.packageName)) {
// bad token
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
}
应用层调用到 WMS,进行 View 的添加。parentWindow 初始化为 null。并且 type 为默认的 TYPE_APPLICATION,继续运行 parentWindow 值没有改变。hasParent 初始化为 false,该 View 没有父容器。token 的值为 attrs.token,为 null。
rootType 为 TYPE_APPLICATION,接着调用 unprivilegedAppCanCreateTokenWith 方法:
private boolean unprivilegedAppCanCreateTokenWith(WindowState parentWindow,
int callingUid, int type, int rootType, IBinder tokenForLog, String packageName) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
ProtoLog.w(WM_ERROR, "Attempted to add application window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (rootType == TYPE_INPUT_METHOD) {
ProtoLog.w(WM_ERROR, "Attempted to add input method window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (rootType == TYPE_VOICE_INTERACTION) {
ProtoLog.w(WM_ERROR,
"Attempted to add voice interaction window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (rootType == TYPE_WALLPAPER) {
ProtoLog.w(WM_ERROR, "Attempted to add wallpaper window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (rootType == TYPE_QS_DIALOG) {
ProtoLog.w(WM_ERROR, "Attempted to add QS dialog window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
ProtoLog.w(WM_ERROR,
"Attempted to add Accessibility overlay window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
if (doesAddToastWindowRequireToken(packageName, callingUid, parentWindow)) {
ProtoLog.w(WM_ERROR, "Attempted to add a toast window with unknown token "
+ "%s. Aborting.", tokenForLog);
return false;
}
}
return true;
}
满足第一个 if 条件,结果返回 false,所以 WMS 的 addWindow 方法会返回 WindowManagerGlobal.ADD_BAD_APP_TOKEN。
回去继续看 ViewRootImpl setView 方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) {
mWindowAttributes.copyFrom(attrs);
attrs = mWindowAttributes;
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {// -1
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token // null
+ " is not valid; is your activity running?");
}
}
}
最终会抛出 BadTokenException 异常,如文章开头所示。
3. 解决方法
根据以上分析流程可知,如果我们将 LayoutParams 的 type 值,改为系统支持的,就不会报如上错误。
WindowManager.LayoutParams.type 赋值策略:
| Android 版本 | 推荐类型 | 常量值 | 特性说明 |
|---|---|---|---|
| Android O+ | TYPE_APPLICATION_OVERLAY | 2038 | 官方推荐,需权限 |
| Android N- | TYPE_SYSTEM_ALERT | 2003 | 传统系统弹窗 |
| 特殊场景 | TYPE_SYSTEM_ERROR | 2010 | 最高优先级(需系统签名) |
并且需要进行权限声明:<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
实现示例:
public class MyService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (!canDrawOverlayWindows(this)) {
requestOverlayPermission(this);
// 可在此处添加逻辑:用户开启权限后重新启动 Service
stopSelf(); // 关闭当前 Service,等待用户手动重启
} else {
// 已拥有权限,执行悬浮窗相关逻辑
showFloatingWindow();
}
return START_NOT_STICKY;
}
private boolean canDrawOverlayWindows(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
return Settings.canDrawOverlays(context);
}
private void requestOverlayPermission(Context context) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + context.getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
private void showFloatingWindow() {
// 实现你的悬浮窗逻辑
}
}
此处留下一个问题,如果 kill Service 进程,View 会不会消去,为什么?