Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android5.0L退出APP橫豎屏切換導致的觸摸屏輸入(Touch Event)無效(凍屏)問題分析(Key Event仍然有效)

Android5.0L退出APP橫豎屏切換導致的觸摸屏輸入(Touch Event)無效(凍屏)問題分析(Key Event仍然有效)

編輯:關於Android編程

一、問題現象

1、多次進出需要強制橫屏的app,比如Real FootBall2015,在退出app的時候會有概率出現退出卡頓,然後TP無法輸入的問題。

2、出問題時Power key有響應。

3、此問題同時在Driver only上有復現。

 

Platform:MSM8916

Android版本:5.0.2L

BuildType:user

系統軟件版本:vA6P+L5P0

系統RAM:1GB

 

參考機行為:

1、ALTO4.5TMO在同樣的簡單測試下沒有重現此問題,後經分析代碼發現是4.4.4KK與5.0.2L在機制上的區別,下面會有具體的對比分析過程。

2、Nexus4和Nexus5在同樣的簡單測試下沒有重現此問題,因沒有源碼所以無法Debug和打印log,後續會嘗試獲取nexus的源碼以了解它的修改方案。

 

二、解決方案

通過初步分析、深入分析、對比分析(具體分析過程、關鍵代碼和log在下面會附上)我們清楚的知道了問題發生的原因、流程以及正常情況下的流程,在這個問題中有很多條件都起到了關鍵作用,最終促成了發生問題的死循環。如果是緊急的備用方案我們可以在任何關鍵條件的環節進行條件判斷處理將出問題的這種情況加以避免,但是如果是長期的最終解決方案我們就要從問題的源頭進行解決。

明確一下問題的根本原因:

1、動畫的執行依賴VSYNC信號,但是VSYNC信號的到來具有規律性,不到16.66ms不能強行發生

2、WIN DEATH和APP DEATH具有不規律性,在任何時間點都有可能發生

3、退出Window需要橫屏切換到豎屏,需要凍結屏幕和輸入

4、解凍屏幕和恢復輸入需要所有Window都Rotate ready

5、出現問題時退出的窗口需要在屏幕和輸入沒有凍結之前執行退場動畫的狀態處理,然後才能正常finishExit滿足Rotate ready條件

一旦上面的退出窗口沒有在凍結屏幕和輸入之前執行一次退場動畫的狀態處理就會成為觸發問題死循環的導火索。

 

針對以上問題的根本原因,我們給出以下解決方案:

1、掐斷問題的導火索

在不對凍結和動畫進行大機制上的同步改動的前提下,對已經凍結VSYNC才來執行退出窗口的退場動畫的情況下進行狀態處理,由於已經凍結,所以退場動畫不需要再執行,需要做的是清除這個待執行的動畫,確保執行finishExit的正常清理操作,從而滿足Rotate ready條件,讓系統的正常機制來進行恢復處理。AOSP的這塊代碼已經考慮到了VSYNC先到來但是還有窗口動畫的一種情況,但是沒有考慮到退場動畫的情況,所以在不做大影響的改動下我們在AOSP原來的機制上新增一種退場動畫的條件判斷,達到解決問題的目的。

 

2、原生代碼和注釋

/

通過以上代碼可以清楚的看到如果屏幕已經凍結okToDisplay就會為false,因此if裡面的代碼主體就不會被執行到,因此動畫的狀態就不會被更新,所以下面就會直接返回。另外我們還可以看到還有一個否則條件以及它的注釋,清楚的說明了如果屏幕已經凍結但是還有一個窗口動畫(不是退場動畫),那麼就清除這個動畫,確保執行下面的清理代碼,但是很遺憾這個條件沒有把當前這個問題的退場動畫包含,所以就無法執行正常的finishExit的清理代碼,finishExit中的工作也至關重要,具體代碼如下:

/

通過以上代碼我們可以看到finishExit中主要做了幾個關鍵動作,首先finishExit當前窗口的所有子窗口,然後將窗口加入待銷毀surface的列表中,同時將mExiting置為false,這個false非常重要,直接影響我們之前分析的apptoken的mDeferRemoval是否能被正常刪除,同時將窗口加入待刪除的列表中,這個是真正刪除的列表。

 

3、最終修改的代碼方案

我們需要修改一行代碼在原來的機制流程上比較完美的解決這個問題,具體代碼如下:

/

如果屏幕和輸入已經凍結,但是窗口的退場動畫還沒有在執行,那麼將正常執行動畫的狀態置為true,確保正常執行下面的finishExit,最終恢復正常流程,解決問題。

 

三、問題初步分析

以Idol347出問題時候的一份典型log為例,發現出問題時log中打出大量如下信息:

/

通過查看代碼,發現上面的log是在InputDispatcher的dispatchOnceInnerLocked中打出來的,具體如下:

/

通過以上代碼我們可以發現,是mDispatchFrozen條件成立才打印出的這句log。

mDispatchFrozen條件是什麼時候被置成true的?代碼中找答案:

/

通過上面的代碼我們可以發現是在setInputDispatchMode函數中設置的mDispatchFrozen,接下來繼續看哪裡調用的setInputDispatchMode,通過查看代碼發現是從WindowManagerService一路調下來的,中間經過了JNI,具體如下:

/

/

/

/

什麼場景下會讓WindowManagerService凍結的輸入?通過查看代碼和添加log,我們可以查看出問題時具體的調用棧信息:

/

通過具體調用棧我們可以發現是ActivityManagerService在處理app死亡通知時,會resume下一個app,在resume的過程中會去調用WindowManagerService的方法檢查是否需要轉屏,如果需要轉屏則調用startFreezingDisplayLocked凍結顯示,在凍結顯示的過程中會凍結輸入:

/

知道了凍結輸入的場景,接下來還有一個更重要的問題,什麼場景下會讓WindowManagerService恢復正常輸入?有凍結就有解凍,繼續查看代碼和調用棧信息:

/

/

從調用棧信息中我們可以發現在處理app died通知並resume下一個app的過程中會調用解凍顯示,解凍顯示的過程中會解凍輸入,但是從log中可以看到,出問題時解凍顯示函數在還沒有走到解凍輸入的時候就因為mWindowsFreezingScreen條件為true而返回了,因此輸入沒有恢復正常。初步分析到這裡已經定位到第一個問題點:mWindowsFreezingScreen為true導致不能正常解凍而恢復輸入。但是mWindowsFreezingScreen條件為什麼為true?難道只有這一個地方會去解凍恢復嗎?帶著這兩個問題我們繼續分析。

 

四、深入分析問題

經過初步我們定位到了第一個問題點,同時也產生了兩個問題,接下來我們繼續深入分析以期能到找到答案和問題的根本原因。

1、mWindowsFreezingScreen條件為什麼為true?

2、還有什麼地方會解凍恢復正常輸入?

通過查看代碼發現mWindowsFreezingScreen有兩個地方置為true,一個地方置為false:

/

/

/

通過以上代碼我們可以知道在執行updateRotationUncheckedLocked的時候如果需要轉屏則會會將mWindowsFreezingScreen置為true一次,然後每次調用makeWindowFreezingScreenIfNeededLocked的時候如果屏幕已經frozen,也會將mWindowsFreezingScreen置為true。而將mWindowsFreezingScreen置為false的地方只有一個,置為false的同時也會解凍輸入。這也間接回答了我們的第二個問題,除了初步分析中的第一個解凍輸入的地方,還有一個解凍輸入的地方,那就是performLayoutAndPlaceSurfacesLockedInner:

/

這個函數是WindowManagerService非常重要的一個函數,根據名字我們可以知曉其功能的一二,他裡面主要執行布局、計算、窗口的移除以及動畫的調度等各種狀態管理,是調用頻率非常高的一個函數,只要窗口狀態有任何的變化都會執行到這裡。到這如果mWindowsFreezingScreen想要被置為false,還需要滿足一個條件,那就是mInnerFields.mOrientationChangeComplete必須為true,我們繼續追蹤mInnerFields.mOrientationChangeComplete何時被置為true,發現只有一個地方mInnerFields.mOrientationChangeComplete會被置為true,具體代碼如下:

/

分析到這可以看到與Animation開始產生關系了,繼續追蹤調用關系,發現copyAnimToLayoutParamsLocked是在WindowAnimator的animateLocked中調用的,而animateLocked是由VSYNC信號來了之後由Choreographer的FrameDisplayEventReceiver調用的,具體調用棧如下:

/

在調用copyAnimToLayoutParamsLocked之前,animateLocked會先調用updateWindowsLocked去更新所有應用的動畫,包括正在退出和已經刪除的應用,然後還會調用WindowStateAnimator的prepareSurfaceLocked去做相應的狀態計算,具體代碼如下:

/

在執行updateWindowsLocked時會調用WindowStateAnimator的stepAnimationLocked,這個函數在當前這個問題中有非常關鍵的作用,下面會著重介紹。

由於ActivityManagerService在處理app died的時候並沒有與WindowManagerService處理Window died和執行動畫進行同步,因此就有可能出現Window died的退場動畫還沒有來得及等到下一個VSYNC(16.666ms一次)執行一次動畫操作,就被ActivityManagerService在resume下一個需要轉屏的應用時凍結屏幕和輸入,在下一個VSYNC來了之後去執行window died的退場動畫時發現屏幕已經凍結,從而不能正常finishExit的window而直接返回,成為這個問題的一個最關鍵的點。

Window died的關鍵處理代碼如下:

/

/

/

performLayoutAndPlaceSurfacesLocked最終會調用到performLayoutAndPlaceSurfacesLockedInner,然後就會執行窗口大小的計算和相關狀態更新,其中影響此問題非常關鍵的操作是調用updateResizingWindows:

/

由於window dead,所以window和可視內容以及大小發生了變化,因此會調用makeWindowFreezingScreenIfNeededLocked,這個函數中會判斷屏幕是否已經凍結,如果已經凍結則會將mInnerFields.mOrientationChangeComplete一直置為false,雖然WindowAnimator會調用copyAnimToLayoutParamsLocked將mInnerFields.mOrientationChangeComplete置為true,但是因為執行copyAnimToLayoutParamsLocked之後仍然需要調用requestTraversalLocked去執行performLayoutAndPlaceSurfacesLocked,所以會被makeWindowFreezingScreenIfNeededLocked再次置為false,其實performLayoutAndPlaceSurfacesLocked的執行過程中會對Window的內容和大小變化進行更新,正常情況下執行makeWindowFreezingScreenIfNeededLocked的條件不會一直滿足,具體代碼如下:

/

/

但是當前這種情況比較特殊,因為Window已經結束,所以調用mClient.resized會發生RemoteException,導致上面代碼中的狀態不能被置為false,從而導致調用makeWindowFreezingScreenIfNeededLocked的條件一直滿足,最終使WindowManagerService不能解凍屏幕和恢復輸入,一旦屏幕先凍結,這裡會與WindowStateAnimator的stepAnimationLocked的處理一起形成一個不能解凍和恢復正常輸入的死循環。

/

/

死循環在哪裡?

1、Window退出調用WindowManagerService的removeWindowLocked

2、removeWindowLocked會執行退場動畫,並調用performLayoutAndPlaceSurfacesLocked進行一次計算和狀態處理,並將動畫進行調度處理,放入下一個VSYNC的處理列表中,因為動畫還沒有被執行處理所以mInnerFields.mOrientationChangeComplete不為true,因此mWindowsFreezingScreen也不會被置為false,屏幕和輸入不會被解凍和恢復。

3、ActivityManagerService接收到app died的通知之後resume下一個app,下一個app與當前結束的這個app的orientation不一樣,觸發凍結屏幕和輸入。

4、VSYNC到來,執行動畫的相關操作,因為屏幕已經被凍結,所以正在退出的Window不能執行動畫操作而直接返回,導致finishExit不能被執行,最終Window不會被正常刪除。執行copyAnimToLayoutParamsLocked將mInnerFields.mOrientationChangeComplete置為true,然後調用requestTraversalLocked發送執行下一次performLayoutAndPlaceSurfacesLocked的消息到消息隊列中。

5、performLayoutAndPlaceSurfacesLocked執行,調用updateResizingWindows。因為退出的Window沒有被finishExit,並且執行reportResized更新窗口大小和內容狀態的過程中由於Window已經退出,所以調用mClient.resized執行IPC(跨進程調用)時發生RemoteException,導致關鍵狀態值沒有被置位清空,所以執行updateResizingWindows的過程中會因為Window的狀態一直滿足條件而調用makeWindowFreezingScreenIfNeededLocked,因為此時窗口已經被凍結,所以會將mInnerFields.mOrientationChangeComplete一直置為false,因此不會將mWindowsFreezingScreen置為false和調用stopFreezingDisplayLocked解凍屏幕和恢復輸入。接著會調用scheduleAnimationLocked將下一次動畫調度到VSYNC的列表中。

6、下一次VSYNC到來,重復第四步和第五步構成不能解凍屏幕和恢復輸入的死循環

 

五、KK4.4.4與L5.0.2的機制區別

1、L5.0.2新增條件mDeferRemoval

接收到app的dead通知之後,ActivityManagerService會調用WindowManagerService的removeAppToken,具體代碼和調用關系如下:

/

/

在removeAppToken的過程中,KK4.4.4與L5.0.2有一些區別,L5.0.2新增加了一個條件mDeferRemoval,為了這個處理這個條件L5.0.2新增加一些代碼來一起完成這個機制特性,關鍵具體代碼如下:

/

/

/

/

/

/

/

/

KK4.4.4的關鍵具體代碼如下:

/

/

mDeferRemoval這個條件會影響mExitingAppTokens中apptoken的刪除和與apptoken關聯的Window的刪除,而且這個條件與正在執行動畫或者正在退出也強相關,通過上面的分析和代碼我們知道發生問題時WindowManagerService存在一個死循環,因此在執行performLayoutAndPlaceSurfacesLocked的過程中調用checkForDeferredActions時,stack.isAnimating()條件會一直滿足,因為有正在退出的窗口還沒有finishExit,因此不會做mExitingAppTokens的刪除,所以apptoken會一直存在,與這個apptoken關聯的Window也會一直存在。

KK4.4.4沒有mDeferRemoval這個條件,所以會在performLayoutAndPlaceSurfacesLocked的過程中直接刪除apptoken。

另外KK4.4.4在同樣先凍結屏幕再來VSYNC執行動畫的情況下Window同樣不會被finishExit而保留在WindowList中,但是由於KK4.4.4沒有mDeferRemoval的機制,所以在rebuildAppWindowListLocked的時候會將不能正常被finishExit但是它的apptoken已經從task和exitingAppTokens中刪除的窗口刪除,同時MTK還加了一個patch用來徹底remove Window防止窗口洩露,具體代碼如下:

/

/

/

所以雖然在退出強制橫屏的窗口進入launcher不能正常finishExit的時候,在不進入其他app的情況下可以通過adb shell dumpsys |grep “game”看到這個窗口還在,但是當你進入其他app窗口的時候會調用handleAppTransitionReadyLocked,然後再調用rebuildAppWindowListLocked將其刪除,這裡你在執行上面的命令就看不到不能finishExit的窗口了,原因就是上面分析的機制和代碼所致。

2、L5.0.2使用帶有虛擬按鍵的NavigationBar

因為L5.0.2使用帶有虛擬按鍵的NavigationBar,所以在退出強制橫屏app的窗口回到Launcher時,與KK4.4.4手機使用實體按鍵在config change時的布局條件不同。L5.0.2在布局時因為從全屏顯示到退出到launcher會發生rotate引起config change,所以會執行一次布局,同時需要更新顯示NavigationBar區域,所以在執行computeFrameLw過程中mContentFrame會發生變化,進而引起mContentInsets的變化,最終win.setInsetsChanged()條件滿足,由於前面說到的執行reportResized更新窗口大小和內容狀態的過程中由於Window已經退出,所以調用mClient.resized執行IPC(跨進程調用)時發生RemoteException,導致關鍵狀態值沒有被置位清空,所以這裡也是同樣情況,布局條件會因此一直滿足,performLayoutLockedInner中具體的關鍵代碼如下:

/

/

win.mLayoutSeq這個條件會影響到窗口freezing狀況的保持以及mInnerFields.mOrientationChangeComplete狀態的置位,具體的關鍵代碼如下:

/

/

執行窗口rotate之後的布局參數log如下:

/

圈紅的參數中cf是contentframe的矩形大小,它會與frame做計算,得出一個ci,因為從全屏橫屏到launcher的變化,所以NavigationBar會被計算布局和刷新,因此ci與上次不同w.setInsetsChanged();滿足為true,同時w.mContentInsetsChanged為true。

KK4.4.4因為沒有使用NavigationBar所以上面的條件不會滿足,通過實驗我將KK4.4.4的NavigationBar打開,同樣情況下上面的條件也會滿足,但是KK4.4.4依然不會凍屏,因為我參考的KK4.4.4有MTK的patch,增加一個條件:mDisplayFrozenTimeout,當WINDOW_FREEZE_TIMEOUT之後,mDisplayFrozenTimeout會被置為true,所以makeWindowFreezingScreenIfNeededLocked函數中保持屏幕凍結狀態的代碼不會被走到,具體如下:

/

另外我參考的KK4.4.4還有一處修改也會直接影響不會發生此問題,在WindowAnimator中加了一處判斷轉屏動畫結束並解凍恢復輸入的代碼,具體如下:

/

以上這些區別讓我參考的MTK的KK4.4.4不會出現凍屏不能輸入的問題。

 

六、後續動作和其他相關問題

1、嘗試獲取Nexus的源碼以了解它的解決方案

2、在Window Freezing timeout的時候強制解凍和恢復輸入的臨時方案以及它會引起的問題:

/

引起的第一個問題:為什麼每次退出強制橫屏的應用到豎屏都會Window Freezing timeout?

分析:由於正在退出的窗口沒有被finishExit,所以它會一直存在並影響mInnerFields.mOrientationChangeComplete條件一直為false,所以不會正常執行解凍恢復的代碼以及app transition的代碼,最終導致超時後強制解凍和恢復輸入,關鍵代碼如下:

/

 

/

引起的第二個問題:為什麼在同一個應用執行轉屏動作也會等到Window Freezing timeout才進行轉屏?

與第一個問題是同樣原理,由於有一個沒有被finishExit的窗口,所以stopFreezingDisplayLocked不會被及時的正常執行,而stopFreezingDisplayLocked中會進行ScreenRotationAnimation的dismiss操作,dismiss會將ScreenRotationAnimation給start起來,然後進行ScreenRotationAnimation,關鍵的具體代碼如下:

/

/

 

Analyzed by vincent.song from SWD2 Framework team.

[email protected]

201504231130

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved