一个诡异的BadTokenException

在我们的Bugly上一直有一个排名较高的崩溃android.view.WindowManager$BadTokenException,堆栈是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
android.view.WindowManager$BadTokenException Unable to add window -- token android.os.BinderProxy@432011d8 is not valid; is your activity running?
android.view.ViewRootImpl.setView(ViewRootImpl.java:594)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:269)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3099)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2350)
android.app.ActivityThread.access$1100(ActivityThread.java:139)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:136)
android.app.ActivityThread.main(ActivityThread.java:5315)
java.lang.reflect.Method.invokeNative(Native Method)
java.lang.reflect.Method.invoke(Method.java:515)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:864)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:680)
dalvik.system.NativeStart.main(Native Method)

这个是一个非常正常的Activity启动堆栈,一脸黑线。StackOverflow上一搜这个崩溃基本都是Dialog有关,那么这个是一个什么诡异的情况,我们研究了好几次,都没有什么结果。最近偶然发现了一点蛛丝马迹,顺藤摸瓜下去,终于找到了原因。

首先从源码来探究一下这个BadTokenException是怎么出来的吧:

里面的”Token”是什么?

我们这个崩溃出现的地方全部都是在启动的第一个Activity中(即Launcher点击图标启动Activity),那就从这部分代码开始研究。

每一个ActivityRecord里面都有一个appToken变量,它是一个Binder对象,主要用于维持该Activity在AMS里与WindowManager之间的联系。它在ActivityRecord的构造函数中被初始化,通过调用WindowManager.addAppToken方法将该Token注册到WindowManagerService里面。

那么ActivityRecord是什么时候被初始化的呢?每一个Activity都在AMS里面有一个对应的ActivityRecord,在startActivity中的第一阶段就被初始化,并向WindowManagerService添加Token,我画了一张图来表示这个过程:

1

WindowManagerService.addAppToken函数中,它对传入的token进行了一层包装,为了理解后面的内容,这里简单介绍一下这个函数:

(代码做了大量精简,有兴趣的同学可以自己阅读)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//WindowManagerService
public void addAppToken(int addPos, IApplicationToken token, int taskId, int stackId,
int requestedOrientation, boolean fullscreen, boolean showForAllUsers, int userId,
int configChanges, boolean voiceInteraction, boolean launchTaskBehind) {
// 一些七七八八的代码
AppWindowToken atoken = findAppWindowToken(token.asBinder());
if (atoken != null) {
Slog.w(TAG, "Attempted to add existing app token: " + token);
return;
}
atoken = new AppWindowToken(this, token, voiceInteraction);
// 对Token的初始化和赋值操作
task.addAppToken(addPos, atoken);
mTokenMap.put(token.asBinder(), atoken); //存入mTokenMap里
//一些七七八八的代码
}

它根据传入的token新建了一个AppWindowToken对象,然后以传入的token为key,新建的对象为值放入了一个Map中。

我们再配合源码,可以看到先添加要启动的Activity的Token到WMS中,之后才会做下面几个操作:

  • 处理已Resumed的、正在Paused中的Activity,让他们进入Paused状态;
  • ActivityThread.main()为入口启动进程,初始化Context和Application;
  • 启动目标Activity。

可以参考部分罗升阳的Activity启动流程,他分析的很好,可惜里面的Android版本有点老了。有兴趣的同学可以再看新的代码,我的分析是根据6.0的源码的。

现在回头看看报错是在哪一步:handleResumeActivity。那必然是在上面添加Token之后的事情了,我们追一下看看发生了什么:

堆栈最后一步 ViewRootImpl.addView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//七七八八的代码
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
//七七八八的代码
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
// 就是这里崩了
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");

这里的mWindowSession,它是一个Binder,具体的实现位于Session.java中。ViewRootImpl在初始化的时候会调用WindowManagerGlobal.getWindowSession()对它进行初始化,并保存在WindowManagerGlobal的静态变量sWindowSession中。之后Activity就能够通过这个Session通知WindowManager添加、删除View。

Session.addToDisplay函数中我们可以看到它是直接透传给了WindowManagerService,并调用它的addWindow函数,我们看看它里面是怎么返回的。

  1. WindowManagerService.addWindow
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // WindowManagerService
    public int addWindow(Session session, IWindow client, int seq,
    WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
    Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
    InputChannel outInputChannel) {
    WindowToken token = mTokenMap.get(attrs.token); //从mTokenMap中拿传入token的对应AppWindowToken
    if (token == null) { //妈个鸡,没找到
    if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
    Slog.w(TAG, "Attempted to add application window with unknown token "
    + attrs.token + ". Aborting.");
    return WindowManagerGlobal.ADD_BAD_APP_TOKEN; //这里就返回了
    }
    // 剩下的代码都不重要了
    }

我们看到,这里就返回了WindowManagerGlobal.ADD_BAD_APP_TOKEN,然后ViewRootImpl那里就跑到了switch分支里面,报出崩溃。

好的,这是之前所有的研究成果,根本毫无头绪,怎么token突然没了?!这个bug就暂时搁置了。

突然发现关键信息

说到这里,不得不感谢一个StackOverflow,它里面一个回答给了我提示:

The change in my app that makes this crash possible is android:noHistory=”true” in Activity declaration of AndroidManifest.xml.

没错,我们的启动Activity确实标记了noHistory。虽然后面经过研究,他猜测的原因错了,但是他提供了复现方法:在Application里面做一个特别耗时的行为,然后此时切回Launcher,等待一会,就会崩溃。

其实这个之前不是没有考虑过,我曾经试过在Application里面sleep 10秒,没有崩。然后这次我直接sleep 30秒,崩了。

那去除noHistory呢?没崩。

mengbi

官方文档说加了这个标志之后,不会被保留在Task中。这个标志是为什么加的其实我也不知道,一开始就有了。好的吧,那到底为什么加了它就崩了?这得研究一下吧。

其实光从这里,我也看不出什么,但是我在复现的时候,注意到了一个地方,sleep久的时候,也就是造成崩溃的时候,系统打出来的日志里面总会多出一行

09-11 17:08:51.917 873-963/? W/ActivityManager: Activity destroy timeout for ActivityRecord{426e9e20 u0 com.desmond.testapplication/.MainActivity t21}

每次只要有它,就一定会崩。那就有一个这个猜想:

因为设置了noHistory为true的Activity在Stop时(按Home键触发)会强制触发destroy操作,但是此时Activity的主线程卡在了Application中无法顺利进行下面的操作,AMS判定Destroy超时,于是将它强制销毁了!

追溯源码

首先我们看一下AMS对于设置了noHistory=true的Activity是怎么处理的,有一处关键的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ActivityStack.java
final void stopActivityLocked(ActivityRecord r) {
if ((r.intent.getFlags()&Intent.FLAG_ACTIVITY_NO_HISTORY) != 0
|| (r.info.flags&ActivityInfo.FLAG_NO_HISTORY) != 0) {
if (!r.finishing) {
if (!mService.isSleeping()) { // 设备没有进入Sleep
if (requestFinishActivityLocked(r.appToken, Activity.RESULT_CANCELED, null, //这里finish
"stop-no-history", false)) {
// Activity was finished, no need to continue trying to schedule stop.
adjustFocusedActivityLocked(r, "stopActivityFinished");
r.resumeKeyDispatchingLocked();
return;
}
} else {
if (DEBUG_STATES) Slog.d(TAG_STATES, "Not finishing noHistory " + r
+ " on stop because we're just sleeping");
}
}
}

通过上面这段代码能够发现,对于noHistory的Activity,它确实会在stop的时候被AMS触发一个finish操作。那么这就能解释的通了,不过整个过程还是比较复杂,我将它缕一下。

先看看Destroy Timeout 是哪里发出来的:

1
2
3
4
5
6
7
8
9
10
11
12
//ActivityStack.java
final boolean destroyActivityLocked(ActivityRecord r, boolean removeFromApp, String reason) {
//...
if (r.finishing && !skipDestroy) {
if (DEBUG_STATES) Slog.v(TAG_STATES, "Moving to DESTROYING: " + r
+ " (destroy requested)");
r.state = ActivityState.DESTROYING;
Message msg = mHandler.obtainMessage(DESTROY_TIMEOUT_MSG, r);
mHandler.sendMessageDelayed(msg, DESTROY_TIMEOUT);
}
//...
}

注意这个mHandler是在AMS进程里的Handler,它使用的是ActivityManagerService里面成员变量mHandlerLooper,不会被Client-Side(即APP进程的操作所影响)。它在调用destroyActivityLocked的时候会先给Handler发一个延迟的TIMEOUT_MSG,如果client端顺利执行完了destroy,则会通知ActivityStack移除这个MSG,否则它就会被执行。

ActivityStack在收到这个消息以后会做一系列操作,可以参考这个图,我们可以看到最后它会把WindowManagerService中存的appToken给remove掉!

2

然后注意,这个时候我们的Application还卡着呢!它现在卡在ActivityThread.handleBindApplication里面,换句话说,它的Handler卡在处理BIND_APPLICATION中了,无法处理之后的消息。自然所有的LAUNCH/RESUME/DESTROY全部都处理不了! 那么这个时候WindowToken会被Timeout移除,接下来它在处理排队中的消息时,处理到RESUME,走到最开始说的ActivityThread.handleResumeActivity=>ViewRootImpl.addView,发现竟然找不到我的Token了!崩了。

没错,配合log,配合现象,这就是这个bug的唯一解释。

解释了后半部分,前半部分还不太清楚。DESTROY_TIMEOUT_MSG发送出来的呢?换句话说,stopActivityLocked是谁调用的呢?既然研究了就继续看一下。

按下Home键实际上是启动LauncherActivity,它是一个启动另外一个进程Activity的过程。我们可以通过按下Home键打出的ActivityManager的Log看出来:

ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.HOME]

在LauncherActivity走到Resume过程的时候,ActivityThread.handleResumeActivity里面会调用Looper.myQueue().addIdleHandler(new Idler());添加一个IdleHandler,当Looper消息循环结束,进入空闲状态时会触发它的回调,象征着我现在客户端的主进程已经”Idle”了,随时待命。在这个IdleHandler里面调用ActivityManagerService.activityIdle函数。这里触发后续的操作,如图所示:

3

ps:这也印证了A start B,肯定是B的resume走完,再走A的stop的。

另外的尝试

之前分析到,是在handleResumeActivity的时候才向WindowManagerService添加View的,那么如果是其他声明了noHistory=true的Activity在onCreate中做超长耗时,并按了Home键出去,会不会一样崩呢?

各位看官可以自己写一个Demo来验证一下。

解决办法

要说根本原因,肯定是不能再主线程里面做太多事情,特别是会在Application与Activity的onCreate阶段。如果说要把这个Bug解了,就把noHistory去掉,那stop不会强制触发finish,自然就没有DESTROY_TIMEOUT,AMS就不会强制干掉你的WindowToken了。

参考文章: