[TOC]
# 背景
近幾個月來收到了多起在Android11手機上,拖拽界面時無法滑動的問題反饋。 表現為在異常的界面上按住屏幕進行**滑動沒有任何響應,但又可以進行點擊**。而除了這個界面,**其他界面一切正常**。
## 復現場景
在B界面(個人主頁)發送事件(取消關注某個作者),界面A(列表界面)收到事件,進行RemoveData(移除對應作者的作品), 然后調用RecyclerView.Adapter#notifyDataSetChange操作通知刷新。再返回到A界面,此時的A界面變變得無法滑動,但可以點擊。再點擊進入其他界面C,C界面都可正常滑動。
## 被合并的Move事件
大部分的滑動問題都是因為存在著嵌套滑動沖突。為了驗證是否是嵌套的問題,我們需要在不同層級的View中打印接收到的MotionEvent. 很快,我們就排除了嵌套滑動的因素。因為當我們在Activity#dispatchTouchEvent的時候對MotionEvent進行打印,驚奇的發現MotionEvent在分發到Activity的時候就已經“**不同尋常**”。
1 . 手指在按壓滑動過程中不會收到任何Move事件。Move事件在手指抬起后,跟隨Up事件一并發送,并且有僅只有一個Move事件。
2. 通過查看這個“唯一”的Move事件,發現其MotionEvent#getHistorySize()竟然達到幾十上百,存放著Move過程中的所有軌跡點。
# 排查流程
## 系統進程 InputReader&InputDispatcher

InputReader 和 InputDispatcher 是跑在System Server進程中的里面的兩個 Native 線程,負責**讀取和分發 Input 事件**。要想分析input事件的流向,需要從這里開始入手。
1. InputReader 讀取 Input 事件
2. InputReader 將讀取的 Input 事件放到 InboundQueue 中
3. InputDispatcher 從 InboundQueue 中取出 Input 事件派發到目標 Connection 的 OutBoundQueue(即發送給哪個Window是由InputDispatcher決定的)
4. 同時將事件記錄到各個 Connection 的 WaitQueue
5. App 接收到 Input 事件,同時記錄到 PendingInputEventQueue ,然后對事件進行分發處理
6. App 處理完成后,回調 InputManagerService 將負責監聽的 WaitQueue 中對應的 Input 移除
### 工具dumpsys
```
adb shell dumpsys input
```
出現問題的界面進行滑動,同時手指保持再屏幕上,不進行抬起,進行是adb shell dumpsys input 可以看到OutboundQueue中是沒有任何東西的,而WaitQueue中堆積了大量的MotionEvent(action=MOVE),此時也并沒有被合并成一個。

## APP進程
### UP/DOWN 與 MOVE
在每一幀的時候應用只能對一次input事件進行響應反饋。如果在一個VSYNC周期中出現了多個input事件,每次input事件到來的時候都立即分發到應用層是比較浪費資源的。為了避免浪費,就有了Batched Consumption機制,input事件會被進行批處理,然后在每個Frame渲染時發送一個batched input事件給到應用層。
### 工具 dump
```
使用AndroidStudio Profile查看Java調用棧 使用AndroidStudio Profile工具,選擇CPU,觸摸界面并進行record,dump文件之后,可以看到java層的代碼調用。(AS也可以進行native調用棧的查看)
```
出現問題的界面進行滑動,同時手指保持再屏幕上,不進行抬起,進行是adb shell dumpsys input 可以看到OutboundQueue中是沒有任何東西的,而WaitQueue中堆積了大量的MotionEvent(action=MOVE),此時也并沒有被合并成一個。
#### 正常情況Consume Batched MoveEvent

#### 異常情況Consume Batched MoveEvent

## ViewRootImpl
從前面的java堆棧圖中,我們可以看到java層是主動調用了一個doConsumeBatchedInput來進行input事件消費的。而這個doConsumeBatchedInput與兩個Runnable有關ConsumeBatchedInputRunnable 和 ConsumeBatchedInputImmediatelyRunnable
**ConsumeBatchedInputRunnable 和 ConsumeBatchedInputImmediatelyRunnable**ConsumeBatchedInputRunnable和ConsumeBatchedInputImmediatelyRunnable這兩個是ViewRootImpl中定義的Runnable,他們都會調用到native方法nativeConsumeBatchedInputEvents讀取inputChannel中的input event,前者是等到下一個Frame繪制的時候再執行input事件消費。后者如其名稱immediately,是立即進行input事件的消費,**常用于一些異常場景下的事件清零操作**。 與此對應的有mConsumeBatchInputScheduled和mConsumeBatchInputImmediatelyScheduled這兩個變量,來標識是否已經將對應的Runnable添加到MessageQueue里面,避免加入重復的Runnable。在對應Runnable的內部執行中又會把這個變量置為false。
### Android11的改動
**改動點1: ViewRootImpl#scheduleConsumeBatchedInput**

這里對ConsumeBatchedInputRunnable的添加新增了一個開關變量mConsumeBatchedImmediatelyScheduled,使得“延時消費input”和“立即消費input”變成兩個互斥的操作。
**改動點2: ViewRootImpl#setWindowStopped**

setWindowStopped中新增調用一次scheduleConsumeBatchedInputImmdiately()。目的是在window切換為stopped狀態后為了避免ANR,調用scheduleConsumeBatchedInputImmdiately()**立即**進行一次input事件消費 也就是在這里mConsumeBatchedInputImmediatelyScheduled這個變量被置為true,從結果上來說,這個Runnable并沒有被執行!
### 排查思路 遍歷父布局
針對這兩次的修改,我們大膽猜測mConsumeBatchInputImmediatelyScheduled這個在置為true之后,出現了某種異常,對應的ConsumeBatchedInputImmediatelyRunnable并沒有被執行,該變量并沒有被置為false,導致另外一個ConsumeBatchedInputRunnable不滿足執行條件,進而引發事件消費異常。Move Event沒有被應用消費,導致界面無法滑動。那么我們如何進行驗證呢?
ViewRootImpl是框架層的類,代碼層沒法直接引用到,但畢竟是萬view之祖,我們可以拿到DecorView,再拿到DecorView的父View來得到ViewRootImpl,進而探訪這個ViewRootImpl對象。

### scheduleConsumeBatchedInputImmediately

View中的getHandler()為什么會是ViewRootImpl$ViewRootHandler?先看下源碼中View中是怎么取到handler的。
## 總結下滑動問題的鏈路流程:
1. 我們業務對一個Stop的界面A進行了列表數據的remove
2. 回到界面A,觸發onStart,在Framework的ViewRootImpl會在此時,觸發一次scheduleTraversals準備下一幀的界面重繪,在Android 11的版本上,還會額外調用一個ConsumeBatchedInputImmediatelyRunnable,因為scheduleTraversals會觸發同步屏障,這個ConsumeBatchedInputImmediatelyRunnable并不會被立即運行,必須等到下一幀開始繪制后才可以運行
3. 繪制開始performTraversal中會調用到onMeasure,onLayout和onDraw等流程,由于我們進行了RecyclerView數據的移除,會觸發到RecyclerView#onLayout,然后觸發部分ItemView的onDetachedFromWindow
4. 在這個onDetachedFromWindow中我們調用了getHandler().removeCallbacksAndMessages(null),將target同為ViewRootImpl$ViewRootHandler的ConsumeBatchedInputImmediatelyRunnable從消息隊列中移除。
5. 渲染結束,但是ConsumeBatchedInputImmediatelyRunnable并沒有被執行,mConsumeBatchInputImmediatelyScheduled卻已經被置為true,沒有被重置為false
6. 觸摸屏幕,底層Down事件分發正常
7. 當底層Input事件中的Move事件到來,觸發了onBatchedInputEventPending,觸發到scheduleConsumeBatchedInput,因為Android 11版本新增了對mConsumeBatchInputImmediatelyScheduled開關變量檢測,沒有往下觸發流程,導致move事件沒有被消費。
8. 底層Up事件正常分發,順帶將前面被阻塞的Batched Move事件上傳
## 應對方案
這個滑動問題,造成的因素有Android 11框架層的一個冗余調用,也有業務側對View#getHandler().removeCallbacks(null)系列方法的不規范調用。我們業務已經對內部存量的View#getHandler().removeCallbacks(null)調用進行替換和移除。考慮到Android 11框架層這個冗余調用會在短期內一直存在,同時也很難保證所有開發和第三方庫在此系列方法上的規范調用,我們會維持臨時修復方案。
## 參考資料
[系統級bug解決分享:騰訊開發工程師刨根問底安卓端滑動異常](https://www.163.com/dy/article/GDU0B40U0518R7MO.html)
[dumpsys](https://developer.android.google.cn/studio/command-line/dumpsys)
## 系統源碼
1. https://cs.android.com/android/platform/superproject**推薦,優點是可以進行搜索,速度也挺快的**
2. https://android.googlesource.com/**推薦,AOSP開源代碼倉庫,優點是可以查看最新的代碼和提交記錄**
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺