目录

关于Token

Activity的Destroy过程:

解决方案:

一: 检查消息队列

二: 借助Handler处理消息机制

总结


关于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强制移除了。

        

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐