关于android.view.WindowManager$BadTokenException的解决方法
关于Tokenandroid.view.WindowManager$BadTokenException: Unable to add window是不是经常遇到这个bug,通常的解决方式,是对activity加判断和保护if(!isDestroyed() && !isFinishing())然而,过几天会发现,这个崩溃,在日志平台上还是一直会上报,保护居然没有起到作用。要想彻底解决
目录
关于Token
android.view.WindowManager$BadTokenException: Unable to add window
是不是经常遇到这个bug,通常的解决方式,是对activity加判断和保护
if(!isDestroyed() && !isFinishing())
然而,过几天会发现,这个崩溃,在日志平台上还是一直会上报,保护居然没有起到作用。要想彻底解决,我们首先先来理解这个token是什么?
/frameworks/base/services/core/java/com/android/server/am/ActivityRecord.java
ActivityRecord(ActivityManagerService _service, ProcessRecord _caller, int _launchedFromPid,
int _launchedFromUid, String _launchedFromPackage, Intent _intent, String _resolvedType,
ActivityInfo aInfo, Configuration _configuration,
ActivityRecord _resultTo, String _resultWho, int _reqCode,
boolean _componentSpecified, boolean _rootVoiceInteraction,
ActivityStackSupervisor supervisor, ActivityOptions options,
ActivityRecord sourceRecord) {
service = _service;
appToken = new Token(this, _intent);
info = aInfo;
launchedFromPid = _launchedFromPid;
launchedFromUid = _launchedFromUid;
launchedFromPackage = _launchedFromPackage;
userId = UserHandle.getUserId(aInfo.applicationInfo.uid);
intent = _intent;
shortComponentName = _intent.getComponent().flattenToShortString();
resolvedType = _resolvedType;
componentSpecified = _componentSpecified;
rootVoiceInteraction = _rootVoiceInteraction;
mLastReportedConfiguration = new MergedConfiguration(_configuration);
resultTo = _resultTo;
resultWho = _resultWho;
字面理解就是标识,类似于和服务端通信接口的token,用来识别、鉴权等,这里的appToken在ActivityRecord构造函数中初始化,确保了activity在AMS中的唯一标识,后面的WMS中的windowtoken也是通过这个token来创建。
/frameworks/base/services/core/java/com/android/server/am/ActivityStack.java
TaskRecord task = null;
if (!newTask) {
// If starting in an existing task, find where that is...
boolean startIt = true;
for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
task = mTaskHistory.get(taskNdx);
if (task.getTopActivity() == null) {
// All activities in task are finishing.
continue;
}
if (task == r.task) {
// Here it is! Now, if this is not yet visible to the
// user, then just add it without starting; it will
// get started when the user navigates back to it.
if (!startIt) {
if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Adding activity " + r + " to task "
+ task, new RuntimeException("here").fillInStackTrace());
task.addActivityToTop(r);
r.putInHistory();
//ActivityRecord的token传入WindowManager
mWindowManager.addAppToken(task.mActivities.indexOf(r), r.appToken,
r.task.taskId, mStackId, r.info.screenOrientation, r.fullscreen,
(r.info.flags & ActivityInfo.FLAG_SHOW_FOR_ALL_USERS) != 0,
r.userId, r.info.configChanges, task.voiceSession != null,
r.mLaunchTaskBehind);
if (VALIDATE_TOKENS) {
validateAppTokensLocked();
}
ActivityOptions.abort(options);
return;
}
在WMS中:
synchronized(mWindowMap) {
AppWindowToken atoken = findAppWindowToken(token.asBinder());
if (atoken != null) {
Slog.w(TAG, "Attempted to add existing app token: " + token);
return;
}
//实例化AppWindowToken
atoken = new AppWindowToken(this, token, voiceInteraction);
atoken.inputDispatchingTimeoutNanos = inputDispatchingTimeoutNanos;
atoken.appFullscreen = fullscreen;
atoken.showForAllUsers = showForAllUsers;
atoken.requestedOrientation = requestedOrientation;
atoken.layoutConfigChanges = (configChanges &
(ActivityInfo.CONFIG_SCREEN_SIZE | ActivityInfo.CONFIG_ORIENTATION)) != 0;
atoken.mLaunchTaskBehind = launchTaskBehind;
if (DEBUG_TOKEN_MOVEMENT || DEBUG_ADD_REMOVE) Slog.v(TAG, "addAppToken: " + atoken
+ " to stack=" + stackId + " task=" + taskId + " at " + addPos);
Task task = mTaskIdToTask.get(taskId);
if (task == null) {
task = createTaskLocked(taskId, stackId, userId, atoken);
}
task.addAppToken(addPos, atoken);
mTokenMap.put(token.asBinder(), atoken);
// Application tokens start out hidden.
atoken.hidden = true;
atoken.hiddenRequested = true;
//dump();
}
在 WMS 内部,会根据传入的 appToken 去查找是否已经创建过 WindowToken,如果没有则实例化一个 WindowToken,并将 token 作为标识。
AMS通过ActivityRecord,TaskRecord以及ActivityStack三个类来管理activity,ActivityRecord伴随着Activity的启动而创建,也伴随着Activity的终止而销毁。
Activity的Destroy过程:
boolean skipDestroy = false;
//调用销毁生命周期
try {
if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this);
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
DestroyActivityItem.obtain(finishing, configChangeFlags));
} catch (Exception e) {
// We can just ignore exceptions here... if the process has crashed, our death
// notification will clean things up.
if (finishing) {
removeFromHistory(reason + " exceptionInScheduleDestroy");
removedFromHistory = true;
skipDestroy = true;
}
}
nowVisible = false;
// If the activity is finishing, we need to wait on removing it from the list to give it
// a chance to do its cleanup. During that time it may make calls back with its token
// so we need to be able to find it on the list and so we don't want to remove it from
// the list yet. Otherwise, we can just immediately put it in the destroyed state since
// we are not removing it from the list.
if (finishing && !skipDestroy) {
if (DEBUG_STATES) {
Slog.v(TAG_STATES, "Moving to DESTROYING: " + this + " (destroy requested)");
}
setState(DESTROYING,
"destroyActivityLocked. finishing and not skipping destroy");
// 启动超时线程
mAtmService.mH.postDelayed(mDestroyTimeoutRunnable, DESTROY_TIMEOUT);
} else {
if (DEBUG_STATES) {
Slog.v(TAG_STATES, "Moving to DESTROYED: " + this + " (destroy skipped)");
}
setState(DESTROYED,
"destroyActivityLocked. not finishing or skipping destroy");
if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during destroy for activity " + this);
app = null;
}
在ActivityRecord的destroyImmediately方法中,我们可以看到,先通过scheduleTransaction去调用了activity的生命周期,同时,启动了一个超时线程mDestroyTimeoutRunnable,超时时间为10s,如果10s内没有完成ondestroy生命周期的调用,AWS会将该activity标记为删除,同时token被移除。
如何解决我们的问题呢?
解决方案:
一: 检查消息队列
既然消息队列中,有删除的消息DESTROY_ACTIVITY没有被执行,一直等到超时,也没有被执行到,说明消息还在队列中,如果能拿到该消息,是不是说明这个时候是badtoken,不能做弹窗逻辑
这个方法需hook处理MessageQueue,判断里面的消息类型,受到系统版本限制,容易发生反射失败。
二: 借助Handler处理消息机制
基本流程如上图所示,Destroy Messsage是在MessageQuene里面,如果超时后,Token被移除了,这个时候,再去做弹窗这样的逻辑,就会badToken,那是不是可以在消息队列里面通过发送消息的形式,增加一个弹窗的消息,弹窗消息一定会在Destroy Messsage之后,处理了Destroy Messsage之后,就会使mActivity.isDestroyed()为true,具体解决代码如下:
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
// 注意:通过主线程,post一个message
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
// 二次判断
if (activity != null && !activity.isFinishing()
&& !activity.isDestroyed()) {
//弹窗逻辑,或addview
}
}
});
}
总结
Bug的复现其实很难,自己操经过分析可以知道,一般在应用长期退到后台,这样有可能会 Stop Activity 过多,或横竖屏变化,总之,最终会导致服务端 AMS 主动向当前 Activity 发送 Destory 请求,并且会设置超时空时间,如果Activity在10s内正常处理完成,则标记Activity已销毁,如果不能完成,则通过超时机制,强制移除Token,这个时候,如果触发UI线程去addView,就会导致BadToken,因为服务端AMS将WMS的WindowToken强制移除了。
更多推荐
所有评论(0)