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