背景
最近App開發同事發現了個系統Bug, Dialog顯示後, 電源鍵滅屏後再亮屏, 此時Dialog無法點擊,是個基本上必現的bug, Android系統版本爲Android 7.1.1 .
問題分析
首先發現這個bug後, 可以確定的是這個bug一定是我們自己修改系統相關功能或者代碼引進的, 而不是Android系統本身的問題, 畢竟很容易復現並且暴露給用戶. 首先就來看下點擊失效時的Log, 然後就發現瞭如下Log:
W ViewRootImpl[MainActivity]: Dropping event due to no window focus
可以看到是由於失去焦點而忽略了點擊事件, 因此下面就得從Log打印位置出手跟蹤原始流程來定位爲什麼失去焦點了.
問題定位
在OpenGrok搜索Log中關鍵字, 找到對應函數, 代碼如下:
frameworks/base/core/java/android/view/ViewRootImpl.java
protected boolean shouldDropInputEvent(QueuedInputEvent q) {
if (mView == null || !mAdded) {
Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
return true;
} else if ((!mAttachInfo.mHasWindowFocus
&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped
|| (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
|| (mPausedForTransition && !isBack(q.mEvent))) {
// This is a focus event and the window doesn't currently have input focus or
// has stopped. This could be an event that came back from the previous stage
// but the window has lost focus or stopped in the meantime.
if (isTerminalInputEvent(q.mEvent)) {
// Don't drop terminal input events, however mark them as canceled.
q.mEvent.cancel();
Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);
return false;
}
// Drop non-terminal input events.
Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
return true;
}
return false;
}
可以看到流程走到else if判斷條件裏面去了, 因此我們需要將4個判斷條件值打印出來, 看看是那個條件導致流程走到了這裏, 加入如下Log來打印相關判斷條件:
android.util.Log.e("wenzhe1", "1:" + (!mAttachInfo.mHasWindowFocus && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)));
android.util.Log.e("wenzhe1", "2:" + mStopped);
android.util.Log.e("wenzhe1", "3:" + (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)));
android.util.Log.e("wenzhe1", "4:" + (mPausedForTransition && !isBack(q.mEvent)));
加入Log後, 編譯刷機, 重新復現一下bug, 點擊Dialog後, Log打印如下:
06-15 13:14:10.795 3621 3621 E wenzhe1 : 1:false
06-15 13:14:10.795 3621 3621 E wenzhe1 : 2:true
06-15 13:14:10.795 3621 3621 E wenzhe1 : 3:false
06-15 13:14:10.795 3621 3621 E wenzhe1 : 4:false
可以看到是第二個條件即mStop
爲true導致流程異常了, 看下 mStop
定義位置的註釋,:
// Set to true if the owner of this window is in the stopped state,
// so the window should no longer be active.
boolean mStopped = false;
可以看到, 當Window處於Stop狀態, ViewRootImpl也要置爲Stop狀態, 那麼看到這裏基本對問題出現原因有了大致瞭解: Dialog的Window狀態出現了異常, 亮屏後並沒有將 mStop
置爲false.
接着定位具體問題點, 在當前文件中搜索一下, 可以發現改變mStop
值只有一個地方, 就是 void setWindowStopped(boolean stopped)
函數, 我們在此函數中加個Log打印一下調用堆棧, 看看是那個地方會調用:
void setWindowStopped(boolean stopped) {
if (mStopped != stopped) {
//打印調用堆棧
android.util.Log.e("wenzhe2", android.util.Log.getStackTraceString(new Throwable()));
mStopped = stopped;
final ThreadedRenderer renderer = mAttachInfo.mHardwareRenderer;
if (renderer != null) {
if (DEBUG_DRAW) Log.d(mTag, "WindowStopped on " + getTitle() + " set to " + mStopped);
renderer.setStopped(mStopped);
}
if (!mStopped) {
scheduleTraversals();
} else {
if (renderer != null) {
renderer.destroyHardwareResources(mView);
}
}
}
}
重新復現bug跑下流程, 定位調用的地方爲:
frameworks/base/core/java/android/view/WindowManagerGlobal.java
中的 setStoppedState(IBinder token, boolean stopped)
, 這次我們需要將 mParams
鏈表中的值和token
的值打印出來, 看看滅屏前後裏面值的區別, 來進一步定位問題, 在函數中加入如下Log:
public void setStoppedState(IBinder token, boolean stopped) {
synchronized (mLock) {
int count = mViews.size();
// 加入調試Log
android.util.Log.e("wenzhe3", "view count:" + count + " token:" + token);
for (WindowManager.LayoutParams par : mParams) {
android.util.Log.e("wenzhe3", "params token:" + par.token);
}
for (int i = 0; i < count; i++) {
if (token == null || mParams.get(i).token == token) {
ViewRootImpl root = mRoots.get(i);
root.setWindowStopped(stopped);
}
}
}
}
加入Log後, 又是編譯刷機復現bug跑下流程, 打印Log如下:
// 滅屏Log, 此時操作是將兩個ViewRoot置爲stop狀態
06-15 13:02:09.503 3621 3621 E wenzhe3 : view count:2 token:android.os.BinderProxy@5b7e3c
06-15 13:02:09.504 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
06-15 13:02:09.504 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
// 亮屏Log 此時操作是將兩個ViewRoot置爲非stop狀態
06-15 13:02:12.859 3621 3621 E wenzhe3 : view count:2 token:android.os.BinderProxy@5b7e3c
06-15 13:02:12.859 3621 3621 E wenzhe3 : params token:android.os.BinderProxy@5b7e3c
06-15 13:02:12.859 3621 3621 E wenzhe3 : params token:null
看到這裏, 很容易發現, 滅屏後再亮屏, mParams
鏈表中有一個token變爲null, 導致亮屏時, 兩個ViewRoot(一個Activity的, 一個Dialog的)只有第一個執行了 setWindowStopped()
, 所以 Dialog的ViewRoot的mStop
在亮屏時沒有被置爲 false, 所以後續的點擊事件被忽略了. 問題原因找到了, 現在就要定位是誰導致了這個原因.
同樣在當前文件中搜索 mParams
, 查找添加和刪除元素的地方, 打印Log定位, 步驟和上面類似, 就不貼代碼了, 最後定位是在 void updateViewLayout(View view, ViewGroup.LayoutParams params)
中, 添加的LayoutParams的token是null, 所以導致後面的問題, 同樣打印調用堆棧, 繼續定位具體是哪裏調用的, 添加Log如下:
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams);
// token 爲 null就打印調用堆棧, 定位具體調用位置
if (wparams.token == null)
android.util.Log.e("wenzhe2", android.util.Log.getStackTraceString(new Throwable()));
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
mParams.remove(index);
mParams.add(index, wparams);
root.setLayoutParams(wparams, false);
}
}
打印結果如下:
E wenzhe2 : java.lang.Throwable
E wenzhe2 : at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:383)
E wenzhe2 : at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:101)
E wenzhe2 : at android.app.Dialog.onWindowAttributesChanged(Dialog.java:723)
E wenzhe2 : at android.view.Window.dispatchWindowAttributesChanged(Window.java:1098)
E wenzhe2 : at com.android.internal.policy.PhoneWindow.dispatchWindowAttributesChanged(PhoneWindow.java:2940)
E wenzhe2 : at android.view.Window.setAttributes(Window.java:1129)
E wenzhe2 : at com.android.internal.policy.PhoneWindow.setAttributes(PhoneWindow.java:3804)
E wenzhe2 : at com.android.internal.policy.PhoneWindow$3.onSwipeCancelled(PhoneWindow.java:3039)
E wenzhe2 : at com.android.internal.widget.SwipeDismissLayout.cancel(SwipeDismissLayout.java:307)
E wenzhe2 : at com.android.internal.widget.SwipeDismissLayout$2$1.run(SwipeDismissLayout.java:101)
E wenzhe2 : at android.os.Handler.handleCallback(Handler.java:751)
E wenzhe2 : at android.os.Handler.dispatchMessage(Handler.java:95)
E wenzhe2 : at android.os.Looper.loop(Looper.java:154)
E wenzhe2 : at android.app.ActivityThread.main(ActivityThread.java:6120)
E wenzhe2 : at java.lang.reflect.Method.invoke(Native Method)
E wenzhe2 : at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
E wenzhe2 : at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
看到這個Log, 就確定了問題所在, 引起這個問題的就是我們系統中加入的SwipeDismissLayout(右滑退出)功能引起的, 這個功能之前我有寫過一片文章:點擊傳送, 看下SwipeDismissLayout源碼就知道, 在滅屏的時候會根據當前滑動狀態, 來更新一下window的位置(是cancel還是dismiss), 而更新的時候, 要獲取Window的屬性 WindowManager.LayoutParams newParams = getAttributes();
而此時獲取的屬性中, newParams.token
值爲 null, 所以導致後面一系列問題.
此部分代碼位置: frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
函數爲: private void registerSwipeCallbacks()
, 有興趣可以看下.
解決問題
問題點定位後, bug就好解決了, 先驗證下是不是SwipeDismissLayout引起的, 由於我們系統默認是啓用右滑退出功能的, 默認Dialog也能右滑退出, 所以需要給測試的Dialog加個主題,
主題中加入:<item name="android:windowSwipeToDismiss">false</item>
然後運行測試, bug不復現, 證實就是SwipeDismissLayout引起.所以解決問題基本就兩種方式:
- 給Dialog默認都加上
<item name="android:windowSwipeToDismiss">false</item>
樣式 - 在Dialog源碼中,將PhoneWindow的FEATURE_SWIPE_TO_DISMISS這個feature去掉即可, 這個需要通過添加函數和修改一點PhoneWindow.java中的邏輯來實現.
一些細節
上面只是大概說明了引起問題的原因, 有些細節還沒說明白:爲什麼Dialog內部的PhoneWindow中, 通過getAttributes()得到的LayoutParams.token爲null, 而Activity是正常的, 並且在滅屏之前, mParams
鏈表裏面的token爲什麼是正常的?
LayoutParams.token
是在Window.java中 void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp)
進行賦值的, 而調用此函數是在WindowManagerGlobal.java的addView()
中,代碼如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//部分代碼省略...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
//調用此函數後, wparams.token就不爲null了
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
//部分代碼省略...
}
所以只要parentWindow
不爲空, 則會進行調整, 調整後Token就不爲空了, 所以addView時候添加的LayoutParams的Token不爲空, 所以滅屏前mParams
裏面Token也不爲空.
- addView()階段LayoutParams.token會被重新賦值, 即token已經被存儲到LayoutParams中了, 但滅屏時調用的updateViewLayout()傳遞的LayoutParams.token爲空, 只能說明這兩個不是同一對象, 從前面分析的流程可知, updateViewLayout()函數中的參數LayoutParams是通過調用Window的getAttributes()來得到的, 是當前Window對象的LayoutParams. 而addView()中的的參數LayoutParams則是如下代碼獲得的:
frameworks/base/core/java/android/app/Dialog.java
public void show() {
//部分代碼省略...
WindowManager.LayoutParams l = mWindow.getAttributes();
// 如果滿足條件, 會重新new一個對象, 所以後後續調用adjustLayoutParamsForSubWindow()
//生成的Token並沒有存儲到當前mWindow對象中,後續getAttributes()中的token就爲空
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
nl.copyFrom(l);
nl.softInputMode |=
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
l = nl;
}
mWindowManager.addView(mDecor, l);
//部分代碼省略...
}
上面代碼中, 滿足if判斷條件後, 重新創建了一個對象, 所以後續token並沒有存儲到本身的window中, 通過打印Log, 證實默認情況下if判斷條件爲true, 所以updateViewLayout()時LayoutParams並不是當前Window對象的, 所以後續getAttributes()獲得的LayoutParams.token依然爲null.
我們將if條件內容先直接註釋調, 看看此種方式是否能解決我們遇到的Bug, 實測證明Bug完美解決,
可以看到, 在找到更深層次原因後, 又多了一種解決bug的方法.
總結
- Bug產生的原因是Dialog所在的Window滅屏後, ViewRoot沒有正常執行setWindowStopped()函數導致的.
- 造成setWindowStopped()流程異常原因是使用了SwipeDismissLayout後, 滅屏後會更新當前Window的LayoutParams, 此時通過getAttributes()獲取的LayoutParams中的token爲null, 所以造成後面流程出錯, 禁用SwipeDismissLayout即可解決bug.
- Dialog由於其特殊性, 在調用show()函數時, 會重新創建WindowManager.LayoutParams對象, 導致後面調用addView()的時候, 生成的Token沒有被存儲到當前的Window對象的LayoutParams中, 而是存儲到了new出來的LayoutParams對象中, 所以後續調用getAttributes()獲取的LayoutParams中的token爲null.
總的來說, 分析過程要有耐心, 對問題分析的越深入, 會找到更多解決Bug的方法.