WindowManager当前焦点窗口管理,及多用户下当前焦点window错乱问题

   android中窗口是由WindowManagerService管理的,其中有一个成员变量mCurrentFocus,记录的是当前的焦点窗口,用于将实时input event传递给这个window处理,比如back键。当然在activity切换的时候,这个mCurrentFocus的值会实时变化成当前activity所在的window.这个原生逻辑本来是没有问题的,但是在引入多用户之后,情况却变的复杂了,一个典型的情况就是:如果从桌面直接切换用户的话,新用户的当前窗口无法获取焦点。下面主要针对这个问题,对windowmanager设置mCurrentFocus的过程做一个梳理。

  下面我会对这个问题的完整发现解决过程做个记录,以期帮助改善之后的解bug思路。

   首先问题是什么呢?在切换用户之后,来到新用户的时候,发现menu键、back键无效,这个刚开始困扰了很久,因为当时对windowmanager也不了解,所以根本也没往这个方向想。但我们公司有点好的地方,就是有些牛人,对framework非常熟悉。所以我就请教了其中一个大牛,果不其然,他很快给我指明了思路,当前窗口可能不是焦点窗口。但怎么找到当前焦点窗口呢?adb shell dumpsys window.android已经提供了工具,可以看到当前系统的各种状态,唉,确实积累过少,对这个工具知道来到这个公司才知道。所以诚恳的说,来到这个公司很是学到不少东西的,尤其是系统开发这方面的,因为之前是做app开发的,这些东西没接触过。

  好了,继续回到技术上。通过dumpsys命令查到当用户切换之后,当前的焦点窗口变成null了,而不是新用户的前台窗口。有了这个发现,肯定很激动,至少不会是一个无头苍蝇漫无目的的猜测了,可以有一个明确的思路方向了。下面的问题就变成mCurrentFocus为什么会在切换用户之后变成null的追寻过程了。

 1. 查看WindowManagerService源码,发现:

      private WindowState findFocusedWindowLocked(DisplayContent displayContent) {

                   ............................................

                   if (mFocusedApp == token) {
                            // Whoops, we are below the focused app...  no focus for you!
                            if (localLOGV || DEBUG_FOCUS_LIGHT) Slog.v(TAG,
                                    "findFocusedWindow: Reached focused app=" + mFocusedApp);
                            return null;
                     }

                  ............................................

      }

    这个方法会在每次更新当前焦点窗口computeFocusedWindowLocked()的时候调用,当然直接看这块代码也没有多大信息,最好是debug或者打开log,查看各个变量的赋值情况,经过比对发现了一个奇怪的现象,就是mFocusedApp不是当前的前台activity.这里大概说一个这个变量,就是在每次activity切换的时候,mFocusedApp会被赋值当前的前台activity,当然这个过程是由ActivityManagerService来完成的,所以接下来就是分析mFocusedApp为什么没有被赋值为最前台activity的过程了。

   

 2. 通过反向追踪,mFocusedApp赋值调用关系是这样的

      i.  WindowManagerService. setFocusedApp(IBinder token, boolean moveFocusNow)

          ii. ActivityManagerSrevice.setFocusedActivityLocked(ActivityRecord r) 

             iii ActivityStack.adjustFocusedActivityLocked(ActivityRecord r)

                iiii ActivityStack.stopActivityLocked(ActivityRecord r)

    这个关系图是反向的,就是说调用关系是反过来的,只不过我的追踪过程是反向追踪,所以这样罗列。

    大致说一下这个过程,实际上我们从第4部可以看出来新的mFocusedApp被赋值是因为前一个焦点activity处于onStop状态了(stopActivityLocked),那么它就负责为mFocusedApp附上新的可见activity.这个逻辑肯定是通的,那么出现上述问题肯定是因为在切换用户的时候,上述逻辑的某一步没走通。

   这里面再说一个背景,就是用户切换的是哪两个activity在切换。对于发生我们这个问题的情况实际上是由u0 com.android.launcher/com.android.launcher2.Launcher到 u9 xxxxx,实际上就是从0用户下的桌面activity到9用户下的某个activity.有了这个背景,再debug上述四步调用过程,发现逻辑断在了这里:

      private void adjustFocusedActivityLocked(ActivityRecord r) {
        if (mStackSupervisor.isFrontStack(this)&& mService.mFocusedActivity == r) {
            ..................................................
            mService.setFocusedActivityLocked(mStackSupervisor.topRunningActivityLocked());
        }
      }

     就是被这个if卡在了正常之门之外了,具体点就是mStackSupervisor.isFrontStack(this) = false了。isFrontStack是用于判断当前task所在的stack是否处于前台,这里介绍一下android对于activity的管理机制,因为系统中运行了多个应用,每个应用也会有多个activity,这样android就需要有一套机制来很好的管理他们,就是task. 正常情况下,一个应用里面的所有activity都在一个task里面,当然如果对activity节点指定了taskaffinity属性,他就不会和其它activity处于同一个task了(可以实现一些特殊需求,比如希望每次点击图标都进入MainActivity)。查看TaskRecord源码,发现这一块的数据结构只是一个简单的final ArrayList mActivities = new ArrayList(). 

   说完task了,再介绍ActivityStack.实际上这里面的简单关系是:很多activity组成了TaskRecord,而多个TaskRecord又组成了ActivityStack.那么系统里是怎么划分stack了,分析发现实际正常情况下,只有两个stack,homeStack和非home stack. HomeStack里面记录的只有桌面应用的task,其它打开的所有应用的activity都处于非home stack中。

  那么问题来了,既然是从0用户下桌面acitivity切换到9用户下的activity,那么在切换之前肯定是桌面activity处于前台了,而它所在的stack肯定也应该是front了,那现在事与愿违,只能分析ActivityStackSupervisor.isFrontStack(ActivityStack stack)方法了。

   层层跟进去,发现罪魁祸首是ActivityStackSupervisor中的mStackState变量,它可能赋值是:

       switch (mStackState) {
            case STACK_STATE_HOME_IN_FRONT:
            case STACK_STATE_HOME_TO_FRONT:
                return mHomeStack;
            case STACK_STATE_HOME_IN_BACK:
            case STACK_STATE_HOME_TO_BACK:
            default:
                return mFocusedStack;
        }

    这里很明显就是记录前台stack是否为home stack, debug之后发现mStackState确实不是home。那这就奇怪了,因为当桌面处于前台时,mStackState已经被赋值为STACK_STATE_HOME_IN_FRONT了,这个从log中可以明显的看出来,那为什么之后切换用户的时候mStackState的值又变了呢?苦寻无果,只能把源码中所有对mStackState赋值的地方加上log,分析之后发现原来问题还是出现在切换用户的过程中。 

   

3. ActivityStackSupervisor切换用户时候的操作

      boolean switchUserLocked(int userId, UserStartedState uss) {
        mUserStackInFront.put(mCurrentUser, getFocusedStack().getStackId());
        final int restoreStackId = mUserStackInFront.get(userId, HOME_STACK_ID);
        mCurrentUser = userId;


        mStartingUsers.add(uss);
        for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
            mStacks.get(stackNdx).switchUserLocked(userId);
        }


        ActivityStack stack = getStack(restoreStackId);
        if (stack == null) {
            stack = mHomeStack;
        }
        final boolean homeInFront = stack.isHomeStack();
        moveHomeStack(homeInFront);
        mWindowManager.moveTaskToTop(stack.topTask().taskId);
        return homeInFront;
    }

   标红处就是改变mStackState的地方。大致说一下这个方法的逻辑,实际上就是读取之前存储的要切换的用户的前台activity是什么,同时把它所在的stack置为前台stack,而9用户下的所有activity都处于非home stack中,而最悲剧的是:

  ActivityManagerService:

        switchUser(final int userId){

              ...............................................

                boolean homeInFront = mStackSupervisor.switchUserLocked(userId, uss);
                if (homeInFront) {
                    startHomeActivityLocked(userId);
                } else {
                    mStackSupervisor.resumeTopActivitiesLocked();
                }

              ...............................................

        }

  这个switchUser实际上是切换用户调用的最直接api,发现了吗,mStackSupervisor.switchUserLocked(userId, uss);是在前面被调用,它被调用之后mStackState已经就不是hoem stack了,而接下来才是用户切换过程真正的activity切换(标红处)。所以问题就转到了mStackSupervisor.isFrontStack(this) = false了,当然也就无法进入if判断里面了

  if (mStackSupervisor.isFrontStack(this) && mService.mFocusedActivity == r) {
       。。。。。。。。。。。。。。。。。
            mService.setFocusedActivityLocked(mStackSupervisor.topRunningActivityLocked());
        }

   这样,桌面就无法实现把mFocusedApp赋值的新打开的activity的这个光荣使命了。

   至此,问题已明了,就是切换用户mStackState被提前改变导致的。那么怎么修改呢?直观的想法是修改ActivityManagerService的switch方法中相关逻辑的时序,但这个很危险,因为这个设计还涉及其他问题,不能为了这个bug而整个调换吧。这里面再插入一点,就是android原生怎么会有这么大的bug呢?实际上这个不能说是原生bug,因为这个问题只会在从桌面切换用户时出现,而原生逻辑中切换用户实在锁屏中完成的,所以也就不存在这个问题了。但我公司的产品却必须从桌面切换用户,所以就必须解决这个问题。

  最后的解决思路是:

     if ((mStackSupervisor.isFrontStack(this) || r.userId != mCurrentUser) && mService.mFocusedActivity == r) {
       。。。。。。。。。。。。。。。。。。
            mService.setFocusedActivityLocked(mStackSupervisor.topRunningActivityLocked());
        }

   即或上一个条件,发现如果userId不匹配的话就不进行isFrontStack这个判断了。这个修改是没有问题的,也很轻量级。因为同用户下切换,肯定不会受这个条件影响,只会在用户切换过程中稍微修改了一下逻辑。

  好了,这就是折腾了几天的成果。好好利用dumpsys工具。



你可能感兴趣的:(android多用户)