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<ActivityRecord> mActivities = new ArrayList<ActivityRecord>(). 

   說完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工具。



發佈了37 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章