歡迎使用Markdown編輯器
一. 現象描述
近日測試上報一個幾乎必現的crash,描述如下:
現象: launcher編輯狀態與鎖屏解鎖交互時系統概率性重啟
操作步驟:
- 進入launcher組件編輯狀態
- 按電源鍵滅屏后亮屏,鎖屏界面上滑解鎖
- launcher編輯狀態向右或向左滑動
- 重復1,2,3 步驟多次操作,觀察系統重啟情況
二. 初步分析
隨后在提供的日志中搜索“crash”,果然發現了以下的報錯信息打印:
從日志中大概能分析出3點重要的信息
- crash 發生在system_server進程
- 報錯打印是Could not find consume time for seq=3513
- crash大概發生在input時間在被client端處理完畢后向 inputdispatchar 發送 finsh的階段
除此之外,在大概5s后,crash進一步引起了anr,根據anr的提示信息:** ANR in gesture monitor owned by pid:1045. Reason: PointerEventDispatcher0** 可進一步推斷,**crash發生在 手勢處理的過程中向 inputdispatchar 發送 finsh的階段
**
三. 分析crash發生的原因
猜想1
根據報錯的提示信息和堆棧打印,在結合代碼,報錯發生在frameworks/native/libs/input/InputTransport.cpp中
上文提到發生crash發生在 手勢處理的過程中向 inputdispatchar 發送 finsh的階段,大概流程如下
- client端梳理完事件代用finishInputEvent隨后進一步來到圖中1的位置,
- 根據seq在容器mConsumeTimes中嘗試獲取一個時間戳
- 向input端發送finish消息
- 從容器mConsumeTimes中移除該seq對應的對象
crash發生在階段2,從容器mConsumeTime獲取不到該seq對應的對象,然后報錯 Could not find consume time for seq=3513,然后根據該類的其他代碼得知,mConsumeTime 是一個map類型的容器,在事件分發前,會以事件的seq為key,當前時間為value添加一條記錄,當時間被client端處理完畢后然后從該容器中移除對應seq的記錄。
結合代碼和報錯的注釋,初步猜想是對應事件的finish調用了兩次引發了crash:
為了驗證猜想,隨后放開了input相關的日志,進行復現,發現發生crash時 對應seq的事件確實調用了兩次finish:
猜想是正確的,現在的問題變成了為什么status_t InputConsumer::sendFinishedSignal發生了兩次,
進一步猜想
進一步閱讀代碼得知frameworks/base/core/jni/android_view_InputEventReceiver.cpp中會調用到 InputConsumer::sendFinishedSigna.
對于普通的touch事件來說,調用到InputConsumer::sendFinishedSigna.有兩種情況:
- 情況1
事件被java層的frameworks/base/core/java/android/view/InputEventReceiver.java正常處理完畢后,主動調用finishInputEvent隨后再來到natvie層調用到 InputConsumer::sendFinishedSigna. 如下圖
- 情況二
在時間分發階段,構造了一個java層的事件對象,然后交給java層去處理,java層處理階段出現了異常,native層的NativeInputEventReceiver捕獲到了該異常,NativeInputEventReceiver會發起一次InputConsumer.sendFinishedSignal
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {.......bool skipCallbacks = false;for (;;) {uint32_t seq;........jobject inputEventObj;switch (inputEvent->getType()) {// 構造 MotionEvent 對象case AINPUT_EVENT_TYPE_MOTION: {if (kDebugDispatchCycle) {ALOGD("channel '%s' ~ Received motion event.", getInputChannelName().c_str());}MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {*outConsumedBatch = true;}inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);break;}........default:assert(false); // InputConsumer should prevent this from ever happeninginputEventObj = nullptr;}if (inputEventObj) {if (kDebugDispatchCycle) {ALOGD("channel '%s' ~ Dispatching input event.", getInputChannelName().c_str());}env->CallVoidMethod(receiverObj.get(),// 調用java方法,分發事件gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);if (env->ExceptionCheck()) {// 捕獲到java層的異常,使得skipCallbacks = true;ALOGE("Exception dispatching input event.");skipCallbacks = true;}env->DeleteLocalRef(inputEventObj);} else {ALOGW("channel '%s' ~ Failed to obtain event object.",getInputChannelName().c_str());skipCallbacks = true;}}if (skipCallbacks) {ALOGD("NativeInputEventReceiver consumeEvents seq=%u ", seq);// 因為前面捕獲到異常,skipCallbacks == ture。 調用mInputConsumer.sendFinishedSignalmInputConsumer.sendFinishedSignal(seq, false);}}
}
在上文兩處中添加log,復現crash log如下:
可見對于seq 3513 ,正常情況和發生異常的情況都調用了一次InputConsumer.sendFinishedSignal!!!
總結一下目前的線索:在桌面手勢分發seq == 3513的事件階段出現了異常,異常被native層的NativeInputEventReceiver得知,然后發起了一次sendFinishedSignal, 同時java層在對seq ==3513事件處理結束后,主動調用了一次sendFinishedSignal,兩次調用sendFinishedSignal進而引發crash。
有新的疑問涌出:
- 桌面手勢分發seq == 3513的事件階段出現了什么異常
- 為什么正常的情況和異常的情況都觸發?
四. 發現端倪
在native檢測到異常的分支里面添加對錯誤的打印,然后復現問題,發現如下打印,原來是上層發生可空指針問題
但,這個空指針問題怎么會使得 異常分支和正常分支的sendFnishSignal都觸發呢?隨后在PointerEventDispatcher這個類中發現端倪:
frameworks/base/services/core/java/com/android/server/wm/PointerEventDispatcher.java
@Overridepublic void onInputEvent(InputEvent event) {try {if (event instanceof MotionEvent&& (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {MotionEvent motionEvent = (MotionEvent) event;if (ENABLE_PER_WINDOW_INPUT_ROTATION) {final int rotation = mDisplayContent.getRotation();if (rotation != Surface.ROTATION_0) {mDisplayContent.getDisplay().getRealSize(mTmpSize);motionEvent = MotionEvent.obtain(motionEvent);motionEvent.transform(MotionEvent.createRotateMatrix(rotation, mTmpSize.x, mTmpSize.y));}}PointerEventListener[] listeners;synchronized (mListeners) {if (mListenersArray == null) {mListenersArray = new PointerEventListener[mListeners.size()];mListeners.toArray(mListenersArray);}listeners = mListenersArray;}for (int i = 0; i < listeners.length; ++i) {listeners[i].onPointerEvent(motionEvent);}}} finally { // catch呢?????finishInputEvent(event, false);}}
如上,分發完事件后,無論是否發生異常都會在finally中調用finishInputEvent,同時雖然有try, final,但沒有catch,這就意味著該段邏輯并未捕獲任何類型的異常!!!!,真相大白:
- 為什么正常情況的sendFnishSignal會被觸發? 事件處理結束后觸發了 finally 執行了finishInputEvent,隨后進一步觸發sendFnishSignal
- 為什么異常情況的sendFnishSignal也會被觸發? 首先事件分發階段發生了異常,但PointerEventDispatcher并沒有catch住相關的異常,隨后異常被native層的NativeInputEventReceiver獲知,進入了異常分支也觸發了一次sendFnishSignal。
但,疑問還在繼續,這似乎是google的原生的一個bug,為什么谷歌的代碼這樣處理?無論正常還是異常情況下 java層調用finishInputEvent 觸發一次sendFnishSignal,然后native層檢測到異常 再觸發一次 sendFnishSignal,Java層和native層的代碼不像同一個人寫的,都考慮到了異常情況要去觸發sendFnishSignal,然后就發生了在分發階段出現異常時同一事件就發生了兩次 sendFnishSignal
這么明顯的bug,谷歌應該能發現吧? 果然:
谷歌的修復: 發生異常時native層不再觸發sendFnishSignal